mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add support for cinema mode (#1305)
## Description Adds support for cinema mode, which is enabled by default. When enabled and if starting playback from the beginning, the server is queried for any "intros", ie prerolls or trailers, to play first. These are shown in the queue and can be skipped if desired. The server has to provide the intros, so a plugin like https://github.com/jellyfin/jellyfin-plugin-intros or https://github.com/CherryFloors/jellyfin-plugin-cinemamode is required server-side. ### Related issues Closes #418 Closes #1180 ### Testing Emulator and server w/ https://github.com/CherryFloors/jellyfin-plugin-cinemamode ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
ea6a0227b0
commit
edb37f5d69
11 changed files with 170 additions and 64 deletions
|
|
@ -12,7 +12,7 @@ import java.util.UUID
|
||||||
* This is not the same thing as a Jellyfin server playlist
|
* This is not the same thing as a Jellyfin server playlist
|
||||||
*/
|
*/
|
||||||
class Playlist(
|
class Playlist(
|
||||||
items: List<BaseItem>,
|
items: List<PlaylistItem>,
|
||||||
startIndex: Int = 0,
|
startIndex: Int = 0,
|
||||||
) {
|
) {
|
||||||
val items = items.subList(startIndex, items.size)
|
val items = items.subList(startIndex, items.size)
|
||||||
|
|
@ -23,15 +23,15 @@ class Playlist(
|
||||||
|
|
||||||
fun hasNext(): Boolean = (index + 1) < items.size
|
fun hasNext(): Boolean = (index + 1) < items.size
|
||||||
|
|
||||||
fun getPreviousAndReverse(): BaseItem = items[--index]
|
fun getPreviousAndReverse(): PlaylistItem = items[--index]
|
||||||
|
|
||||||
fun getAndAdvance(): BaseItem = items[++index]
|
fun getAndAdvance(): PlaylistItem = items[++index]
|
||||||
|
|
||||||
fun peek(): BaseItem? = items.getOrNull(index + 1)
|
fun peek(): PlaylistItem? = items.getOrNull(index + 1)
|
||||||
|
|
||||||
fun upcomingItems(): List<BaseItem> = items.subList(index + 1, items.size)
|
fun upcomingItems(): List<PlaylistItem> = items.subList(index + 1, items.size)
|
||||||
|
|
||||||
fun advanceTo(id: UUID): BaseItem? {
|
fun advanceTo(id: UUID): PlaylistItem? {
|
||||||
while (hasNext()) {
|
while (hasNext()) {
|
||||||
val potential = getAndAdvance()
|
val potential = getAndAdvance()
|
||||||
if (potential.id == id) {
|
if (potential.id == id) {
|
||||||
|
|
@ -52,3 +52,22 @@ data class PlaylistInfo(
|
||||||
val count: Int,
|
val count: Int,
|
||||||
val mediaType: MediaType,
|
val mediaType: MediaType,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sealed interface PlaylistItem {
|
||||||
|
val id: UUID
|
||||||
|
val item: BaseItem
|
||||||
|
|
||||||
|
data class Media(
|
||||||
|
override val item: BaseItem,
|
||||||
|
) : PlaylistItem {
|
||||||
|
override val id: UUID
|
||||||
|
get() = item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Intro(
|
||||||
|
override val item: BaseItem,
|
||||||
|
) : PlaylistItem {
|
||||||
|
override val id: UUID
|
||||||
|
get() = item.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -455,6 +455,18 @@ sealed interface AppPreference<Pref, T> {
|
||||||
summaryOff = R.string.disabled,
|
summaryOff = R.string.disabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val CinemaMode =
|
||||||
|
AppSwitchPreference<AppPreferences>(
|
||||||
|
title = R.string.cinema_mode,
|
||||||
|
defaultValue = true,
|
||||||
|
getter = { it.playbackPreferences.cinemaMode },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updatePlaybackPreferences { cinemaMode = value }
|
||||||
|
},
|
||||||
|
summaryOn = R.string.enabled,
|
||||||
|
summaryOff = R.string.disabled,
|
||||||
|
)
|
||||||
|
|
||||||
val RememberSelectedTab =
|
val RememberSelectedTab =
|
||||||
AppSwitchPreference<AppPreferences>(
|
AppSwitchPreference<AppPreferences>(
|
||||||
title = R.string.remember_selected_tab,
|
title = R.string.remember_selected_tab,
|
||||||
|
|
@ -1166,6 +1178,7 @@ val advancedPreferences =
|
||||||
preferences =
|
preferences =
|
||||||
listOf(
|
listOf(
|
||||||
AppPreference.OneClickPause,
|
AppPreference.OneClickPause,
|
||||||
|
AppPreference.CinemaMode,
|
||||||
AppPreference.GlobalContentScale,
|
AppPreference.GlobalContentScale,
|
||||||
AppPreference.SkipSegments,
|
AppPreference.SkipSegments,
|
||||||
AppPreference.MaxBitrate,
|
AppPreference.MaxBitrate,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ class AppPreferencesSerializer
|
||||||
refreshRateSwitching =
|
refreshRateSwitching =
|
||||||
AppPreference.RefreshRateSwitching.defaultValue
|
AppPreference.RefreshRateSwitching.defaultValue
|
||||||
resolutionSwitching = AppPreference.ResolutionSwitching.defaultValue
|
resolutionSwitching = AppPreference.ResolutionSwitching.defaultValue
|
||||||
|
cinemaMode = AppPreference.CinemaMode.defaultValue
|
||||||
|
|
||||||
overrides =
|
overrides =
|
||||||
PlaybackOverrides
|
PlaybackOverrides
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import com.github.damontecres.wholphin.preferences.updateMpvOptions
|
||||||
import com.github.damontecres.wholphin.preferences.updateMusicPreferences
|
import com.github.damontecres.wholphin.preferences.updateMusicPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updatePhotoPreferences
|
import com.github.damontecres.wholphin.preferences.updatePhotoPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
|
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
|
||||||
|
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateScreensaverPreferences
|
import com.github.damontecres.wholphin.preferences.updateScreensaverPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
|
|
@ -327,5 +328,11 @@ class AppUpgradeHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (previous.isEqualOrBefore(Version.fromString("0.6.2-18-g0"))) {
|
||||||
|
appPreferences.updateData {
|
||||||
|
it.updatePlaybackPreferences { cinemaMode = AppPreference.CinemaMode.defaultValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
import com.github.damontecres.wholphin.data.model.Playlist
|
import com.github.damontecres.wholphin.data.model.Playlist
|
||||||
import com.github.damontecres.wholphin.data.model.PlaylistInfo
|
import com.github.damontecres.wholphin.data.model.PlaylistInfo
|
||||||
|
import com.github.damontecres.wholphin.data.model.PlaylistItem
|
||||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||||
import com.github.damontecres.wholphin.ui.components.baseItemKinds
|
import com.github.damontecres.wholphin.ui.components.baseItemKinds
|
||||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||||
import com.github.damontecres.wholphin.ui.gt
|
import com.github.damontecres.wholphin.ui.gt
|
||||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||||
import com.github.damontecres.wholphin.ui.playback.playable
|
import com.github.damontecres.wholphin.ui.playback.playable
|
||||||
|
import com.github.damontecres.wholphin.ui.toBaseItems
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
|
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
|
||||||
|
|
@ -22,6 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.playlistsApi
|
import org.jellyfin.sdk.api.client.extensions.playlistsApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.videosApi
|
import org.jellyfin.sdk.api.client.extensions.videosApi
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
@ -247,14 +250,14 @@ class PlaylistCreator
|
||||||
-> {
|
-> {
|
||||||
val list =
|
val list =
|
||||||
buildList {
|
buildList {
|
||||||
add(BaseItem(item, false))
|
add(PlaylistItem.Media(BaseItem(item, false)))
|
||||||
|
|
||||||
if (item.partCount.gt(1)) {
|
if (item.partCount.gt(1)) {
|
||||||
api.videosApi
|
api.videosApi
|
||||||
.getAdditionalPart(item.id)
|
.getAdditionalPart(item.id)
|
||||||
.content.items
|
.content.items
|
||||||
.map {
|
.map {
|
||||||
BaseItem(it, false)
|
PlaylistItem.Media(BaseItem(it, false))
|
||||||
}.let(::addAll)
|
}.let(::addAll)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -274,14 +277,14 @@ class PlaylistCreator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun List<BaseItemDto>.convertAndAddParts(useSeriesForPrimary: Boolean = false): List<BaseItem> =
|
private suspend fun List<BaseItemDto>.convertAndAddParts(useSeriesForPrimary: Boolean = false): List<PlaylistItem> =
|
||||||
buildList {
|
buildList {
|
||||||
this@convertAndAddParts.forEach { ep ->
|
this@convertAndAddParts.forEach { ep ->
|
||||||
add(BaseItem(ep, useSeriesForPrimary))
|
add(PlaylistItem.Media(BaseItem(ep, useSeriesForPrimary)))
|
||||||
if (ep.partCount.gt(1)) {
|
if (ep.partCount.gt(1)) {
|
||||||
val parts =
|
val parts =
|
||||||
api.videosApi.getAdditionalPart(ep.id).content.items.map { part ->
|
api.videosApi.getAdditionalPart(ep.id).content.items.map { part ->
|
||||||
BaseItem(part, useSeriesForPrimary)
|
PlaylistItem.Media(BaseItem(part, useSeriesForPrimary))
|
||||||
}
|
}
|
||||||
addAll(parts)
|
addAll(parts)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.PlaylistItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.PlaylistCreationResult
|
import com.github.damontecres.wholphin.services.PlaylistCreationResult
|
||||||
|
|
@ -105,9 +106,9 @@ class PlayExternalViewModel
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val queriedItem = api.userLibraryApi.getItem(itemId).content
|
val queriedItem = api.userLibraryApi.getItem(itemId).content
|
||||||
val base =
|
val playlistItem =
|
||||||
if (queriedItem.type.playable) {
|
if (queriedItem.type.playable) {
|
||||||
queriedItem
|
PlaylistItem.Media(BaseItem(queriedItem))
|
||||||
} else if (destination is Destination.PlaybackList) {
|
} else if (destination is Destination.PlaybackList) {
|
||||||
val playlistResult =
|
val playlistResult =
|
||||||
playlistCreator.createFrom(
|
playlistCreator.createFrom(
|
||||||
|
|
@ -134,18 +135,15 @@ class PlayExternalViewModel
|
||||||
navigationManager.goBack()
|
navigationManager.goBack()
|
||||||
return@launchDefault
|
return@launchDefault
|
||||||
}
|
}
|
||||||
r.playlist.items
|
r.playlist.items.first()
|
||||||
.first()
|
|
||||||
.data
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw IllegalArgumentException("Item is not playable and not PlaybackList: ${queriedItem.type}")
|
throw IllegalArgumentException("Item is not playable and not PlaybackList: ${queriedItem.type}")
|
||||||
}
|
}
|
||||||
val item = BaseItem(base, false)
|
|
||||||
val playbackConfig =
|
val playbackConfig =
|
||||||
serverRepository.currentUser.value?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
itemPlaybackDao.getItem(user, base.id)?.let {
|
itemPlaybackDao.getItem(user, playlistItem.id)?.let {
|
||||||
Timber.v("Fetched itemPlayback from DB: %s", it)
|
Timber.v("Fetched itemPlayback from DB: %s", it)
|
||||||
if (it.sourceId != null) {
|
if (it.sourceId != null) {
|
||||||
it
|
it
|
||||||
|
|
@ -154,20 +152,25 @@ class PlayExternalViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val mediaSource = streamChoiceService.chooseSource(base, playbackConfig)
|
val item =
|
||||||
val plc = streamChoiceService.getPlaybackLanguageChoice(base)
|
when (playlistItem) {
|
||||||
|
is PlaylistItem.Intro -> playlistItem.item
|
||||||
|
is PlaylistItem.Media -> playlistItem.item
|
||||||
|
}
|
||||||
|
val mediaSource = streamChoiceService.chooseSource(item.data, playbackConfig)
|
||||||
|
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||||
if (mediaSource == null) {
|
if (mediaSource == null) {
|
||||||
Timber.w("Media source is null")
|
Timber.w("Media source is null")
|
||||||
return@launchDefault
|
return@launchDefault
|
||||||
}
|
}
|
||||||
savedStateHandle[KEY_ID] = base.id
|
savedStateHandle[KEY_ID] = playlistItem.id
|
||||||
savedStateHandle[KEY_MEDIA_ID] = mediaSource.id
|
savedStateHandle[KEY_MEDIA_ID] = mediaSource.id
|
||||||
val subtitleIndex =
|
val subtitleIndex =
|
||||||
streamChoiceService
|
streamChoiceService
|
||||||
.chooseSubtitleStream(
|
.chooseSubtitleStream(
|
||||||
source = mediaSource,
|
source = mediaSource,
|
||||||
audioStream = null,
|
audioStream = null,
|
||||||
seriesId = base.seriesId,
|
seriesId = item.data.seriesId,
|
||||||
itemPlayback = playbackConfig,
|
itemPlayback = playbackConfig,
|
||||||
plc = plc,
|
plc = plc,
|
||||||
prefs = prefs,
|
prefs = prefs,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.Chapter
|
import com.github.damontecres.wholphin.data.model.Chapter
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
import com.github.damontecres.wholphin.data.model.Playlist
|
import com.github.damontecres.wholphin.data.model.Playlist
|
||||||
|
import com.github.damontecres.wholphin.data.model.PlaylistItem
|
||||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
|
|
@ -185,7 +186,7 @@ class PlaybackViewModel
|
||||||
|
|
||||||
private lateinit var preferences: UserPreferences
|
private lateinit var preferences: UserPreferences
|
||||||
internal lateinit var itemId: UUID
|
internal lateinit var itemId: UUID
|
||||||
internal lateinit var item: BaseItem
|
internal lateinit var currentItem: PlaylistItem
|
||||||
internal var forceTranscoding: Boolean = false
|
internal var forceTranscoding: Boolean = false
|
||||||
private var activityListener: TrackActivityPlaybackListener? = null
|
private var activityListener: TrackActivityPlaybackListener? = null
|
||||||
private val jobs = mutableListOf<Job>()
|
private val jobs = mutableListOf<Job>()
|
||||||
|
|
@ -319,9 +320,9 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
this.itemId = itemId
|
this.itemId = itemId
|
||||||
val queriedItem = api.userLibraryApi.getItem(itemId).content
|
val queriedItem = api.userLibraryApi.getItem(itemId).content
|
||||||
val base =
|
val playlistItem =
|
||||||
if (queriedItem.type.playable) {
|
if (queriedItem.type.playable) {
|
||||||
queriedItem
|
PlaylistItem.Media(BaseItem(queriedItem, false))
|
||||||
} else if (destination is Destination.PlaybackList) {
|
} else if (destination is Destination.PlaybackList) {
|
||||||
val playlistResult =
|
val playlistResult =
|
||||||
playlistCreator.createFrom(
|
playlistCreator.createFrom(
|
||||||
|
|
@ -349,9 +350,7 @@ class PlaybackViewModel
|
||||||
this@PlaybackViewModel.playlist.value = r.playlist
|
this@PlaybackViewModel.playlist.value = r.playlist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.playlist.items
|
r.playlist.items.first()
|
||||||
.first()
|
|
||||||
.data
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -359,10 +358,39 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch(ExceptionHandler()) { controllerViewState.observe() }
|
viewModelScope.launch(ExceptionHandler()) { controllerViewState.observe() }
|
||||||
val item = BaseItem.from(base, api)
|
|
||||||
|
val intros =
|
||||||
|
// If not resuming playback & cinema mode is enabled, get potential intros
|
||||||
|
if (positionMs == 0L && preferences.appPreferences.playbackPreferences.cinemaMode) {
|
||||||
|
api.userLibraryApi
|
||||||
|
.getIntros(
|
||||||
|
itemId = playlistItem.id,
|
||||||
|
userId = serverRepository.currentUser.value?.id,
|
||||||
|
).content.items
|
||||||
|
.map {
|
||||||
|
PlaylistItem.Intro(BaseItem(it))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
val firstItem =
|
||||||
|
if (intros.isNotEmpty()) {
|
||||||
|
Timber.v("Got %s intros", intros.size)
|
||||||
|
val currentPlaylist =
|
||||||
|
this@PlaybackViewModel
|
||||||
|
.playlist.value
|
||||||
|
?.items
|
||||||
|
.orEmpty()
|
||||||
|
val newPlaylist = Playlist(intros + currentPlaylist)
|
||||||
|
this@PlaybackViewModel.playlist.setValueOnMain(newPlaylist)
|
||||||
|
intros.first()
|
||||||
|
} else {
|
||||||
|
playlistItem
|
||||||
|
}
|
||||||
|
|
||||||
val played =
|
val played =
|
||||||
play(
|
play(
|
||||||
item,
|
firstItem,
|
||||||
positionMs,
|
positionMs,
|
||||||
itemPlayback,
|
itemPlayback,
|
||||||
forceTranscoding,
|
forceTranscoding,
|
||||||
|
|
@ -374,9 +402,13 @@ class PlaybackViewModel
|
||||||
if (!isPlaylist && preferences.appPreferences.playbackPreferences.showNextUpWhen != ShowNextUpWhen.NEXT_UP_NEVER) {
|
if (!isPlaylist && preferences.appPreferences.playbackPreferences.showNextUpWhen != ShowNextUpWhen.NEXT_UP_NEVER) {
|
||||||
val result = playlistCreator.createFrom(queriedItem)
|
val result = playlistCreator.createFrom(queriedItem)
|
||||||
if (result is PlaylistCreationResult.Success && result.playlist.items.isNotEmpty()) {
|
if (result is PlaylistCreationResult.Success && result.playlist.items.isNotEmpty()) {
|
||||||
withContext(Dispatchers.Main) {
|
val currentPlaylist =
|
||||||
this@PlaybackViewModel.playlist.value = result.playlist
|
this@PlaybackViewModel
|
||||||
}
|
.playlist.value
|
||||||
|
?.items
|
||||||
|
.orEmpty()
|
||||||
|
val newPlaylist = Playlist(currentPlaylist + result.playlist.items)
|
||||||
|
this@PlaybackViewModel.playlist.setValueOnMain(newPlaylist)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -384,18 +416,24 @@ class PlaybackViewModel
|
||||||
/**
|
/**
|
||||||
* Play an item
|
* Play an item
|
||||||
*
|
*
|
||||||
* @param item the item to play
|
* @param currentItem the item to play
|
||||||
* @param positionMs the starting playback position in milliseconds
|
* @param positionMs the starting playback position in milliseconds
|
||||||
* @param itemPlayback the parameters for playback such chosen subtitle or audio streams
|
* @param itemPlayback the parameters for playback such chosen subtitle or audio streams
|
||||||
* @param forceTranscoding whether the user has requested to force playback via transcoding
|
* @param forceTranscoding whether the user has requested to force playback via transcoding
|
||||||
*/
|
*/
|
||||||
private suspend fun play(
|
private suspend fun play(
|
||||||
item: BaseItem,
|
playlistItem: PlaylistItem,
|
||||||
positionMs: Long,
|
positionMs: Long,
|
||||||
itemPlayback: ItemPlayback? = null,
|
itemPlayback: ItemPlayback? = null,
|
||||||
forceTranscoding: Boolean = this.forceTranscoding,
|
forceTranscoding: Boolean = this.forceTranscoding,
|
||||||
): Boolean =
|
): Boolean =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
val item =
|
||||||
|
when (playlistItem) {
|
||||||
|
is PlaylistItem.Intro -> playlistItem.item
|
||||||
|
is PlaylistItem.Media -> playlistItem.item
|
||||||
|
}
|
||||||
|
|
||||||
Timber.i("Playing ${item.id}")
|
Timber.i("Playing ${item.id}")
|
||||||
|
|
||||||
// New item, so we can clear the media segment tracker & subtitle cues
|
// New item, so we can clear the media segment tracker & subtitle cues
|
||||||
|
|
@ -415,7 +453,7 @@ class PlaybackViewModel
|
||||||
)
|
)
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
}
|
||||||
this@PlaybackViewModel.item = item
|
this@PlaybackViewModel.currentItem = playlistItem
|
||||||
this@PlaybackViewModel.itemId = item.id
|
this@PlaybackViewModel.itemId = item.id
|
||||||
|
|
||||||
val isLiveTv = item.type == BaseItemKind.TV_CHANNEL
|
val isLiveTv = item.type == BaseItemKind.TV_CHANNEL
|
||||||
|
|
@ -875,7 +913,7 @@ class PlaybackViewModel
|
||||||
Timber.d("Changing audio track to %s", index)
|
Timber.d("Changing audio track to %s", index)
|
||||||
val itemPlayback =
|
val itemPlayback =
|
||||||
itemPlaybackRepository.saveTrackSelection(
|
itemPlaybackRepository.saveTrackSelection(
|
||||||
item = item,
|
item = currentItem.item,
|
||||||
itemPlayback = currentItemPlayback.value!!,
|
itemPlayback = currentItemPlayback.value!!,
|
||||||
trackIndex = index,
|
trackIndex = index,
|
||||||
type = MediaStreamType.AUDIO,
|
type = MediaStreamType.AUDIO,
|
||||||
|
|
@ -889,7 +927,7 @@ class PlaybackViewModel
|
||||||
streamChoiceService.resolveSubtitleIndex(
|
streamChoiceService.resolveSubtitleIndex(
|
||||||
source = source,
|
source = source,
|
||||||
audioStreamIndex = index,
|
audioStreamIndex = index,
|
||||||
seriesId = item.data.seriesId,
|
seriesId = currentItem.item.data.seriesId,
|
||||||
subtitleIndex = itemPlayback.subtitleIndex,
|
subtitleIndex = itemPlayback.subtitleIndex,
|
||||||
prefs = preferences,
|
prefs = preferences,
|
||||||
)
|
)
|
||||||
|
|
@ -898,7 +936,7 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
changeStreams(
|
changeStreams(
|
||||||
item,
|
currentItem.item,
|
||||||
itemPlayback,
|
itemPlayback,
|
||||||
index,
|
index,
|
||||||
resolvedSubtitleIndex,
|
resolvedSubtitleIndex,
|
||||||
|
|
@ -913,7 +951,7 @@ class PlaybackViewModel
|
||||||
Timber.d("Changing subtitle track to %s", index)
|
Timber.d("Changing subtitle track to %s", index)
|
||||||
val itemPlayback =
|
val itemPlayback =
|
||||||
itemPlaybackRepository.saveTrackSelection(
|
itemPlaybackRepository.saveTrackSelection(
|
||||||
item = item,
|
item = currentItem.item,
|
||||||
itemPlayback = currentItemPlayback.value!!,
|
itemPlayback = currentItemPlayback.value!!,
|
||||||
trackIndex = index,
|
trackIndex = index,
|
||||||
type = MediaStreamType.SUBTITLE,
|
type = MediaStreamType.SUBTITLE,
|
||||||
|
|
@ -927,7 +965,7 @@ class PlaybackViewModel
|
||||||
streamChoiceService.resolveSubtitleIndex(
|
streamChoiceService.resolveSubtitleIndex(
|
||||||
source = source,
|
source = source,
|
||||||
audioStreamIndex = itemPlayback.audioIndex,
|
audioStreamIndex = itemPlayback.audioIndex,
|
||||||
seriesId = item.data.seriesId,
|
seriesId = currentItem.item.data.seriesId,
|
||||||
subtitleIndex = index,
|
subtitleIndex = index,
|
||||||
prefs = preferences,
|
prefs = preferences,
|
||||||
)
|
)
|
||||||
|
|
@ -936,7 +974,7 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
changeStreams(
|
changeStreams(
|
||||||
item,
|
currentItem.item,
|
||||||
itemPlayback,
|
itemPlayback,
|
||||||
itemPlayback.audioIndex,
|
itemPlayback.audioIndex,
|
||||||
resolvedIndex,
|
resolvedIndex,
|
||||||
|
|
@ -971,7 +1009,7 @@ class PlaybackViewModel
|
||||||
mediaSourceId: UUID? = currentItemPlayback.value?.sourceId,
|
mediaSourceId: UUID? = currentItemPlayback.value?.sourceId,
|
||||||
): String? =
|
): String? =
|
||||||
trickPlayInfo?.let {
|
trickPlayInfo?.let {
|
||||||
val itemId = item.id
|
val itemId = currentItem.id
|
||||||
return api.trickplayApi.getTrickplayTileImageUrl(
|
return api.trickplayApi.getTrickplayTileImageUrl(
|
||||||
itemId,
|
itemId,
|
||||||
trickPlayInfo.width,
|
trickPlayInfo.width,
|
||||||
|
|
@ -982,12 +1020,28 @@ class PlaybackViewModel
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
if (playbackState == Player.STATE_ENDED) {
|
if (playbackState == Player.STATE_ENDED) {
|
||||||
viewModelScope.launchIO {
|
Timber.v("Playback state is STATE_ENDED")
|
||||||
val nextItem = playlist.value?.peek()
|
viewModelScope.launchDefault {
|
||||||
Timber.v("Setting next up to ${nextItem?.id}")
|
when (val nextItem = playlist.value?.peek()) {
|
||||||
withContext(Dispatchers.Main) {
|
is PlaylistItem.Intro -> {
|
||||||
nextUp.value = nextItem
|
Timber.v("Next item is intro, so playing immediately")
|
||||||
if (nextItem == null) {
|
playNextUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlaylistItem.Media -> {
|
||||||
|
if (currentItem is PlaylistItem.Intro) {
|
||||||
|
Timber.v("Current item is intro, so playing next up immediately")
|
||||||
|
playNextUp()
|
||||||
|
} else {
|
||||||
|
Timber.v("Setting next up to ${nextItem.id}")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
nextUp.value = nextItem.item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {
|
||||||
|
Timber.v("No next up")
|
||||||
navigationManager.goBack()
|
navigationManager.goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1053,9 +1107,11 @@ class PlaybackViewModel
|
||||||
outroShownSegments.add(currentSegment.id)
|
outroShownSegments.add(currentSegment.id)
|
||||||
) {
|
) {
|
||||||
val nextItem = playlist.peek()
|
val nextItem = playlist.peek()
|
||||||
Timber.v("Setting next up during outro to ${nextItem?.id}")
|
if (nextItem is PlaylistItem.Media) {
|
||||||
withContext(Dispatchers.Main) {
|
Timber.v("Setting next up during outro to ${nextItem?.id}")
|
||||||
nextUp.value = nextItem
|
withContext(Dispatchers.Main) {
|
||||||
|
nextUp.value = nextItem.item
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val behavior =
|
val behavior =
|
||||||
|
|
@ -1180,7 +1236,7 @@ class PlaybackViewModel
|
||||||
fun playNextUp() {
|
fun playNextUp() {
|
||||||
playlist.value?.let {
|
playlist.value?.let {
|
||||||
if (it.hasNext()) {
|
if (it.hasNext()) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchDefault {
|
||||||
cancelUpNextEpisode()
|
cancelUpNextEpisode()
|
||||||
val item = it.getAndAdvance()
|
val item = it.getAndAdvance()
|
||||||
val played = play(item, 0)
|
val played = play(item, 0)
|
||||||
|
|
@ -1195,7 +1251,7 @@ class PlaybackViewModel
|
||||||
fun playPrevious() {
|
fun playPrevious() {
|
||||||
playlist.value?.let {
|
playlist.value?.let {
|
||||||
if (it.hasPrevious()) {
|
if (it.hasPrevious()) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchDefault {
|
||||||
cancelUpNextEpisode()
|
cancelUpNextEpisode()
|
||||||
val item = it.getPreviousAndReverse()
|
val item = it.getPreviousAndReverse()
|
||||||
val played = play(item, 0)
|
val played = play(item, 0)
|
||||||
|
|
@ -1256,7 +1312,7 @@ class PlaybackViewModel
|
||||||
PlayMethod.DIRECT_STREAM, PlayMethod.DIRECT_PLAY -> {
|
PlayMethod.DIRECT_STREAM, PlayMethod.DIRECT_PLAY -> {
|
||||||
Timber.w("Playback error during ${it.playMethod}, falling back to transcoding")
|
Timber.w("Playback error during ${it.playMethod}, falling back to transcoding")
|
||||||
changeStreams(
|
changeStreams(
|
||||||
item,
|
currentItem.item,
|
||||||
currentItemPlayback.value!!,
|
currentItemPlayback.value!!,
|
||||||
currentItemPlayback.value?.audioIndex,
|
currentItemPlayback.value?.audioIndex,
|
||||||
currentItemPlayback.value?.subtitleIndex,
|
currentItemPlayback.value?.subtitleIndex,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.PlaylistItem
|
||||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.onMain
|
import com.github.damontecres.wholphin.ui.onMain
|
||||||
|
|
@ -106,12 +107,13 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles(
|
||||||
while (maxAttempts > 0 && subtitleCount == newCount) {
|
while (maxAttempts > 0 && subtitleCount == newCount) {
|
||||||
maxAttempts--
|
maxAttempts--
|
||||||
delay(1500)
|
delay(1500)
|
||||||
item =
|
val base = BaseItem(api.userLibraryApi.getItem(itemId = it.itemId).content)
|
||||||
BaseItem.from(
|
currentItem =
|
||||||
api.userLibraryApi.getItem(itemId = it.itemId).content,
|
when (currentItem) {
|
||||||
api,
|
is PlaylistItem.Intro -> PlaylistItem.Intro(base)
|
||||||
)
|
is PlaylistItem.Media -> PlaylistItem.Media(base)
|
||||||
mediaSource = streamChoiceService.chooseSource(item.data, it)
|
}
|
||||||
|
mediaSource = streamChoiceService.chooseSource(currentItem.item.data, it)
|
||||||
if (mediaSource == null) {
|
if (mediaSource == null) {
|
||||||
// This shouldn't happen, but just in case
|
// This shouldn't happen, but just in case
|
||||||
showToast(
|
showToast(
|
||||||
|
|
@ -158,7 +160,7 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
this@downloadAndSwitchSubtitles.changeStreams(
|
this@downloadAndSwitchSubtitles.changeStreams(
|
||||||
item,
|
currentItem.item,
|
||||||
currentItemPlayback.value!!,
|
currentItemPlayback.value!!,
|
||||||
audioIndex,
|
audioIndex,
|
||||||
newStream.index,
|
newStream.index,
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,9 @@ fun QueueRowOverlay(
|
||||||
if (isFocused) controllerViewState.pulseControls()
|
if (isFocused) controllerViewState.pulseControls()
|
||||||
}
|
}
|
||||||
SeasonCard(
|
SeasonCard(
|
||||||
item = item,
|
item = item.item,
|
||||||
onClick = {
|
onClick = {
|
||||||
onClickPlaylist.invoke(item)
|
onClickPlaylist.invoke(item.item)
|
||||||
controllerViewState.hideControls()
|
controllerViewState.hideControls()
|
||||||
},
|
},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ message PlaybackPreferences {
|
||||||
bool refresh_rate_switching = 22;
|
bool refresh_rate_switching = 22;
|
||||||
bool resolution_switching = 23;
|
bool resolution_switching = 23;
|
||||||
string external_player = 24;
|
string external_player = 24;
|
||||||
|
bool cinema_mode = 25;
|
||||||
}
|
}
|
||||||
|
|
||||||
message HomePagePreferences{
|
message HomePagePreferences{
|
||||||
|
|
|
||||||
|
|
@ -767,6 +767,7 @@
|
||||||
<string name="ass_subtitle_mode_exoplayer">Direct play with ExoPlayer built-in</string>
|
<string name="ass_subtitle_mode_exoplayer">Direct play with ExoPlayer built-in</string>
|
||||||
<string name="ass_subtitle_mode_transcode">Burn in/transcode on server</string>
|
<string name="ass_subtitle_mode_transcode">Burn in/transcode on server</string>
|
||||||
<string name="ass_subtitle_playback">SSA/ASS subtitle playback</string>
|
<string name="ass_subtitle_playback">SSA/ASS subtitle playback</string>
|
||||||
|
<string name="cinema_mode">Cinema mode</string>
|
||||||
<string name="show_more">Show %1$s more</string>
|
<string name="show_more">Show %1$s more</string>
|
||||||
<string name="skip_behavior">Skip behavior</string>
|
<string name="skip_behavior">Skip behavior</string>
|
||||||
<string name="skip_behavior_summary">For intros, credits, etc</string>
|
<string name="skip_behavior_summary">For intros, credits, etc</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue