Merge branch 'main' into develop/jellyseerr

This commit is contained in:
Damontecres 2026-01-11 22:27:59 -05:00
commit 84fb998cc5
No known key found for this signature in database
17 changed files with 120 additions and 28 deletions

View file

@ -233,6 +233,7 @@ dependencies {
implementation(libs.androidx.media3.session) implementation(libs.androidx.media3.session)
implementation(libs.androidx.media3.datasource.okhttp) implementation(libs.androidx.media3.datasource.okhttp)
implementation(libs.androidx.media3.exoplayer.hls) implementation(libs.androidx.media3.exoplayer.hls)
implementation(libs.androidx.media3.exoplayer.dash)
implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.ui.compose) implementation(libs.androidx.media3.ui.compose)

View file

@ -414,6 +414,17 @@ sealed interface AppPreference<Pref, T> {
summaryOff = R.string.disabled, summaryOff = R.string.disabled,
) )
val DirectPlayDoviProfile7 =
AppSwitchPreference<AppPreferences>(
title = R.string.force_dovi_profile_7,
defaultValue = false,
getter = { it.playbackPreferences.overrides.directPlayDolbyVisionEL },
setter = { prefs, value ->
prefs.updatePlaybackOverrides { directPlayDolbyVisionEL = value }
},
summary = R.string.force_dovi_profile_7_summary,
)
val RememberSelectedTab = val RememberSelectedTab =
AppSwitchPreference<AppPreferences>( AppSwitchPreference<AppPreferences>(
title = R.string.remember_selected_tab, title = R.string.remember_selected_tab,
@ -992,6 +1003,7 @@ val advancedPreferences =
AppPreference.Ac3Supported, AppPreference.Ac3Supported,
AppPreference.DirectPlayAss, AppPreference.DirectPlayAss,
AppPreference.DirectPlayPgs, AppPreference.DirectPlayPgs,
AppPreference.DirectPlayDoviProfile7,
), ),
), ),
ConditionalPreferences( ConditionalPreferences(

View file

@ -42,6 +42,7 @@ class DeviceProfileService
downMixAudio = prefs.overrides.downmixStereo, downMixAudio = prefs.overrides.downmixStereo,
assDirectPlay = prefs.overrides.directPlayAss, assDirectPlay = prefs.overrides.directPlayAss,
pgsDirectPlay = prefs.overrides.directPlayPgs, pgsDirectPlay = prefs.overrides.directPlayPgs,
dolbyVisionELDirectPlay = prefs.overrides.directPlayDolbyVisionEL,
jellyfinTenEleven = jellyfinTenEleven =
serverVersion != null && serverVersion >= ServerVersion(10, 11, 0), serverVersion != null && serverVersion >= ServerVersion(10, 11, 0),
) )
@ -55,6 +56,7 @@ class DeviceProfileService
downMixAudio = newConfig.downMixAudio, downMixAudio = newConfig.downMixAudio,
assDirectPlay = newConfig.assDirectPlay, assDirectPlay = newConfig.assDirectPlay,
pgsDirectPlay = newConfig.pgsDirectPlay, pgsDirectPlay = newConfig.pgsDirectPlay,
dolbyVisionELDirectPlay = newConfig.dolbyVisionELDirectPlay,
jellyfinTenEleven = newConfig.jellyfinTenEleven, jellyfinTenEleven = newConfig.jellyfinTenEleven,
) )
} }
@ -72,5 +74,6 @@ data class DeviceProfileConfiguration(
val downMixAudio: Boolean, val downMixAudio: Boolean,
val assDirectPlay: Boolean, val assDirectPlay: Boolean,
val pgsDirectPlay: Boolean, val pgsDirectPlay: Boolean,
val dolbyVisionELDirectPlay: Boolean,
val jellyfinTenEleven: Boolean, val jellyfinTenEleven: Boolean,
) )

View file

@ -86,6 +86,7 @@ import com.github.damontecres.wholphin.ui.playback.scale
import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler
@ -525,6 +526,7 @@ fun CollectionFolderGrid(
positionCallback: ((columns: Int, position: Int) -> Unit)? = null, positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
useSeriesForPrimary: Boolean = true, useSeriesForPrimary: Boolean = true,
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions, filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
focusRequesterOnEmpty: FocusRequester? = null,
) = CollectionFolderGrid( ) = CollectionFolderGrid(
preferences, preferences,
itemId.toServerString(), itemId.toServerString(),
@ -541,6 +543,7 @@ fun CollectionFolderGrid(
positionCallback = positionCallback, positionCallback = positionCallback,
useSeriesForPrimary = useSeriesForPrimary, useSeriesForPrimary = useSeriesForPrimary,
filterOptions = filterOptions, filterOptions = filterOptions,
focusRequesterOnEmpty = focusRequesterOnEmpty,
) )
@Composable @Composable
@ -560,6 +563,7 @@ fun CollectionFolderGrid(
positionCallback: ((columns: Int, position: Int) -> Unit)? = null, positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
useSeriesForPrimary: Boolean = true, useSeriesForPrimary: Boolean = true,
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions, filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
focusRequesterOnEmpty: FocusRequester? = null,
playlistViewModel: AddPlaylistViewModel = hiltViewModel(), playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
viewModel: CollectionFolderViewModel = viewModel: CollectionFolderViewModel =
hiltViewModel<CollectionFolderViewModel, CollectionFolderViewModel.Factory>( hiltViewModel<CollectionFolderViewModel, CollectionFolderViewModel.Factory>(
@ -615,6 +619,7 @@ fun CollectionFolderGrid(
pager = pager, pager = pager,
sortAndDirection = sortAndDirection!!, sortAndDirection = sortAndDirection!!,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
focusRequesterOnEmpty = focusRequesterOnEmpty,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = { position, item -> onLongClickItem = { position, item ->
moreDialog.makePresent(PositionItem(position, item)) moreDialog.makePresent(PositionItem(position, item))
@ -759,6 +764,7 @@ fun CollectionFolderGridContent(
currentFilter: GetItemsFilter = GetItemsFilter(), currentFilter: GetItemsFilter = GetItemsFilter(),
filterOptions: List<ItemFilterBy<*>> = listOf(), filterOptions: List<ItemFilterBy<*>> = listOf(),
onFilterChange: (GetItemsFilter) -> Unit = {}, onFilterChange: (GetItemsFilter) -> Unit = {},
focusRequesterOnEmpty: FocusRequester? = null,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -767,7 +773,11 @@ fun CollectionFolderGridContent(
var viewOptions by remember { mutableStateOf(viewOptions) } var viewOptions by remember { mutableStateOf(viewOptions) }
val gridFocusRequester = remember { FocusRequester() } val gridFocusRequester = remember { FocusRequester() }
RequestOrRestoreFocus(gridFocusRequester) if (pager.isNotEmpty()) {
RequestOrRestoreFocus(gridFocusRequester)
} else {
focusRequesterOnEmpty?.tryRequestFocus()
}
var backdropImageUrl by remember { mutableStateOf<String?>(null) } var backdropImageUrl by remember { mutableStateOf<String?>(null) }
var position by rememberInt(initialPosition) var position by rememberInt(initialPosition)

View file

@ -29,6 +29,7 @@ import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
@ -39,13 +40,16 @@ import androidx.compose.ui.unit.sp
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import timber.log.Timber
@Composable @Composable
fun TabRow( fun TabRow(
selectedTabIndex: Int, selectedTabIndex: Int,
tabs: List<String>, tabs: List<String>,
focusRequesters: List<FocusRequester>,
onClick: (Int) -> Unit, onClick: (Int) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -55,7 +59,6 @@ fun TabRow(
state.animateScrollToItem(selectedTabIndex, -(state.layoutInfo.viewportSize.width / 3.5).toInt()) state.animateScrollToItem(selectedTabIndex, -(state.layoutInfo.viewportSize.width / 3.5).toInt())
} }
} }
val focusRequesters = remember(tabs) { List(tabs.size) { FocusRequester() } }
var rowHasFocus by remember { mutableStateOf(false) } var rowHasFocus by remember { mutableStateOf(false) }
LazyRow( LazyRow(
state = state, state = state,
@ -68,6 +71,7 @@ fun TabRow(
onEnter = { onEnter = {
// If entering from left or right, use last or first tab // If entering from left or right, use last or first tab
// Otherwise use the selected tab // Otherwise use the selected tab
Timber.v("onEnter requestedFocusDirection=$requestedFocusDirection, selectedTabIndex=$selectedTabIndex")
val focusRequester = val focusRequester =
if (requestedFocusDirection == FocusDirection.Left) { if (requestedFocusDirection == FocusDirection.Left) {
focusRequesters.lastOrNull() focusRequesters.lastOrNull()
@ -183,6 +187,7 @@ private fun TabRowPreview() {
TabRow( TabRow(
selectedTabIndex = 1, selectedTabIndex = 1,
tabs = listOf("Tab 1", "Tab 2", "Tab 3"), tabs = listOf("Tab 1", "Tab 2", "Tab 3"),
focusRequesters = listOf(),
onClick = {}, onClick = {},
) )
Tab( Tab(

View file

@ -93,14 +93,19 @@ fun CollectionFolderLiveTv(
remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) } remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) }
val folders by viewModel.recordingFolders.observeAsState(listOf()) val folders by viewModel.recordingFolders.observeAsState(listOf())
val tvGuideStr = stringResource(R.string.tv_guide)
val tvDvrStr = stringResource(R.string.tv_dvr_schedule)
val tabs = val tabs =
listOf( remember(folders) {
TabId(stringResource(R.string.tv_guide), UUID.randomUUID()), listOf(
TabId(stringResource(R.string.tv_dvr_schedule), UUID.randomUUID()), TabId(tvGuideStr, UUID.randomUUID()),
) + folders TabId(tvDvrStr, UUID.randomUUID()),
) + folders
}
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val tabFocusRequesters = remember(tabs.size) { List(tabs.size) { FocusRequester() } }
val firstTabFocusRequester = remember { FocusRequester() } val firstTabFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
@ -133,6 +138,7 @@ fun CollectionFolderLiveTv(
.focusRequester(firstTabFocusRequester), .focusRequester(firstTabFocusRequester),
tabs = tabs.map { it.title }, tabs = tabs.map { it.title },
onClick = { selectedTabIndex = it }, onClick = { selectedTabIndex = it },
focusRequesters = tabFocusRequesters,
) )
} }
when (selectedTabIndex) { when (selectedTabIndex) {
@ -150,10 +156,12 @@ fun CollectionFolderLiveTv(
1 -> { 1 -> {
DvrSchedule( DvrSchedule(
true, requestFocusAfterLoading = true,
Modifier focusRequesterOnEmpty = tabFocusRequesters[1],
.fillMaxSize() modifier =
.focusRequester(focusRequester), Modifier
.fillMaxSize()
.focusRequester(focusRequester),
) )
} }
@ -176,6 +184,7 @@ fun CollectionFolderLiveTv(
}, },
playEnabled = false, playEnabled = false,
defaultViewOptions = ViewOptions(), defaultViewOptions = ViewOptions(),
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
) )
} else { } else {
ErrorMessage("Invalid tab index $selectedTabIndex", null) ErrorMessage("Invalid tab index $selectedTabIndex", null)

View file

@ -59,6 +59,7 @@ fun CollectionFolderMovie(
) )
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
val firstTabFocusRequester = remember { FocusRequester() } val firstTabFocusRequester = remember { FocusRequester() }
// LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } // LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
@ -88,6 +89,7 @@ fun CollectionFolderMovie(
.focusRequester(firstTabFocusRequester), .focusRequester(firstTabFocusRequester),
tabs = tabs, tabs = tabs,
onClick = { selectedTabIndex = it }, onClick = { selectedTabIndex = it },
focusRequesters = tabFocusRequesters,
) )
} }
when (selectedTabIndex) { when (selectedTabIndex) {
@ -136,6 +138,7 @@ fun CollectionFolderMovie(
showHeader = position < columns showHeader = position < columns
}, },
playEnabled = true, playEnabled = true,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
) )
} }
@ -168,6 +171,7 @@ fun CollectionFolderMovie(
showHeader = position < columns showHeader = position < columns
}, },
playEnabled = false, playEnabled = false,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
) )
} }

View file

@ -58,6 +58,7 @@ fun CollectionFolderTv(
) )
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
val firstTabFocusRequester = remember { FocusRequester() } val firstTabFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
@ -91,6 +92,7 @@ fun CollectionFolderTv(
.focusRequester(firstTabFocusRequester), .focusRequester(firstTabFocusRequester),
tabs = tabs, tabs = tabs,
onClick = { selectedTabIndex = it }, onClick = { selectedTabIndex = it },
focusRequesters = tabFocusRequesters,
) )
} }
when (selectedTabIndex) { when (selectedTabIndex) {
@ -138,6 +140,7 @@ fun CollectionFolderTv(
preferencesViewModel.navigationManager.navigateTo(item.destination()) preferencesViewModel.navigationManager.navigateTo(item.destination())
}, },
playEnabled = false, playEnabled = false,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
) )
} }

View file

@ -73,6 +73,7 @@ fun FavoritesPage(
stringResource(R.string.playlists), stringResource(R.string.playlists),
stringResource(R.string.people), stringResource(R.string.people),
) )
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -107,6 +108,7 @@ fun FavoritesPage(
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp), .padding(start = 32.dp, top = 16.dp, bottom = 16.dp),
tabs = tabs, tabs = tabs,
onClick = { selectedTabIndex = it }, onClick = { selectedTabIndex = it },
focusRequesters = tabFocusRequesters,
) )
} }
// TODO playEnabled = true for movies & episodes // TODO playEnabled = true for movies & episodes
@ -139,6 +141,7 @@ fun FavoritesPage(
}, },
playEnabled = false, playEnabled = false,
filterOptions = DefaultForFavoritesFilterOptions, filterOptions = DefaultForFavoritesFilterOptions,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
) )
} }
@ -170,6 +173,7 @@ fun FavoritesPage(
}, },
playEnabled = false, playEnabled = false,
filterOptions = DefaultForFavoritesFilterOptions, filterOptions = DefaultForFavoritesFilterOptions,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
) )
} }
@ -202,6 +206,7 @@ fun FavoritesPage(
}, },
playEnabled = false, playEnabled = false,
filterOptions = DefaultForFavoritesFilterOptions, filterOptions = DefaultForFavoritesFilterOptions,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
) )
} }
@ -233,6 +238,7 @@ fun FavoritesPage(
}, },
playEnabled = false, playEnabled = false,
filterOptions = DefaultForFavoritesFilterOptions, filterOptions = DefaultForFavoritesFilterOptions,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
) )
} }
@ -264,6 +270,7 @@ fun FavoritesPage(
}, },
playEnabled = false, playEnabled = false,
filterOptions = DefaultForFavoritesFilterOptions, filterOptions = DefaultForFavoritesFilterOptions,
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
) )
} }
@ -300,6 +307,7 @@ fun FavoritesPage(
}, },
playEnabled = false, playEnabled = false,
filterOptions = listOf(), filterOptions = listOf(),
focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex),
) )
} }

View file

@ -114,6 +114,7 @@ class DvrScheduleViewModel
@Composable @Composable
fun DvrSchedule( fun DvrSchedule(
requestFocusAfterLoading: Boolean, requestFocusAfterLoading: Boolean,
focusRequesterOnEmpty: FocusRequester,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: DvrScheduleViewModel = hiltViewModel(), viewModel: DvrScheduleViewModel = hiltViewModel(),
) { ) {
@ -138,7 +139,13 @@ fun DvrSchedule(
var showDialog by remember { mutableStateOf<BaseItem?>(null) } var showDialog by remember { mutableStateOf<BaseItem?>(null) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
if (requestFocusAfterLoading) { if (requestFocusAfterLoading) {
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } LaunchedEffect(Unit) {
if (active.isNotEmpty() || recordings.isNotEmpty()) {
focusRequester.tryRequestFocus()
} else {
focusRequesterOnEmpty.tryRequestFocus()
}
}
} }
DvrScheduleContent( DvrScheduleContent(
activeRecordings = active, activeRecordings = active,

View file

@ -107,6 +107,18 @@ fun SeriesOverviewContent(
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val scrollConnection = rememberDelayedNestedScroll() val scrollConnection = rememberDelayedNestedScroll()
var requestFocusAfterSeason by remember { mutableStateOf(false) } var requestFocusAfterSeason by remember { mutableStateOf(false) }
val seasonStr = stringResource(R.string.tv_season)
val tabs =
remember(seasons) {
seasons.mapNotNull {
it?.name
?: it?.data?.indexNumber?.let { "$seasonStr $it" }
?: ""
}
}
val focusRequesters = remember(seasons) { List(seasons.size) { FocusRequester() } }
Box( Box(
modifier = modifier =
modifier modifier
@ -138,17 +150,13 @@ fun SeriesOverviewContent(
} }
TabRow( TabRow(
selectedTabIndex = selectedTabIndex, selectedTabIndex = selectedTabIndex,
tabs = tabs = tabs,
seasons.mapNotNull {
it?.name
?: it?.data?.indexNumber?.let { stringResource(R.string.tv_season) + " $it" }
?: ""
},
onClick = { onClick = {
selectedTabIndex = it selectedTabIndex = it
onChangeSeason.invoke(it) onChangeSeason.invoke(it)
requestFocusAfterSeason = true requestFocusAfterSeason = true
}, },
focusRequesters = focusRequesters,
modifier = modifier =
Modifier Modifier
.focusRequester(tabRowFocusRequester) .focusRequester(tabRowFocusRequester)

View file

@ -11,6 +11,7 @@ import androidx.lifecycle.viewModelScope
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.Format import androidx.media3.common.Format
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Tracks import androidx.media3.common.Tracks
@ -46,6 +47,7 @@ import com.github.damontecres.wholphin.services.PlaylistCreationResult
import com.github.damontecres.wholphin.services.PlaylistCreator import com.github.damontecres.wholphin.services.PlaylistCreator
import com.github.damontecres.wholphin.services.RefreshRateService import com.github.damontecres.wholphin.services.RefreshRateService
import com.github.damontecres.wholphin.services.StreamChoiceService import com.github.damontecres.wholphin.services.StreamChoiceService
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.onMain import com.github.damontecres.wholphin.ui.onMain
@ -61,6 +63,7 @@ import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener
import com.github.damontecres.wholphin.util.checkForSupport import com.github.damontecres.wholphin.util.checkForSupport
import com.github.damontecres.wholphin.util.mpv.mpvDeviceProfile import com.github.damontecres.wholphin.util.mpv.mpvDeviceProfile
import com.github.damontecres.wholphin.util.profile.Codec
import com.github.damontecres.wholphin.util.subtitleMimeTypes import com.github.damontecres.wholphin.util.subtitleMimeTypes
import com.github.damontecres.wholphin.util.supportItemKinds import com.github.damontecres.wholphin.util.supportItemKinds
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -598,13 +601,18 @@ class PlaybackViewModel
source?.let { source -> source?.let { source ->
val mediaUrl = val mediaUrl =
if (source.supportsDirectPlay) { if (source.supportsDirectPlay) {
api.videosApi.getVideoStreamUrl( if (source.isRemote && source.path.isNotNullOrBlank()) {
itemId = itemId, Timber.i("Playback is remote for source: %s", source.id)
mediaSourceId = source.id, source.path
static = true, } else {
tag = source.eTag, api.videosApi.getVideoStreamUrl(
playSessionId = response.playSessionId, itemId = itemId,
) mediaSourceId = source.id,
static = true,
tag = source.eTag,
playSessionId = response.playSessionId,
)
}
} else if (source.supportsDirectStream) { } else if (source.supportsDirectStream) {
source.transcodingUrl?.let(api::createUrl) source.transcodingUrl?.let(api::createUrl)
} else { } else {
@ -657,7 +665,12 @@ class PlaybackViewModel
.setMediaId(itemId.toString()) .setMediaId(itemId.toString())
.setUri(mediaUrl.toUri()) .setUri(mediaUrl.toUri())
.setSubtitleConfigurations(listOfNotNull(externalSubtitle)) .setSubtitleConfigurations(listOfNotNull(externalSubtitle))
.build() .apply {
when (source.container) {
Codec.Container.HLS -> setMimeType(MimeTypes.APPLICATION_M3U8)
Codec.Container.DASH -> setMimeType(MimeTypes.APPLICATION_MPD)
}
}.build()
val playback = val playback =
CurrentPlayback( CurrentPlayback(

View file

@ -7,6 +7,7 @@ object Codec {
const val `3GP` = "3gp" const val `3GP` = "3gp"
const val ASF = "asf" const val ASF = "asf"
const val AVI = "avi" const val AVI = "avi"
const val DASH = "dash"
const val DVR_MS = "dvr-ms" const val DVR_MS = "dvr-ms"
const val HLS = "hls" const val HLS = "hls"
const val M2V = "m2v" const val M2V = "m2v"

View file

@ -66,6 +66,7 @@ fun createDeviceProfile(
downMixAudio: Boolean, downMixAudio: Boolean,
assDirectPlay: Boolean, assDirectPlay: Boolean,
pgsDirectPlay: Boolean, pgsDirectPlay: Boolean,
dolbyVisionELDirectPlay: Boolean,
jellyfinTenEleven: Boolean, jellyfinTenEleven: Boolean,
) = buildDeviceProfile { ) = buildDeviceProfile {
val allowedAudioCodecs = val allowedAudioCodecs =
@ -156,6 +157,7 @@ fun createDeviceProfile(
container( container(
Codec.Container.ASF, Codec.Container.ASF,
Codec.Container.DASH,
Codec.Container.HLS, Codec.Container.HLS,
Codec.Container.M4V, Codec.Container.M4V,
Codec.Container.MKV, Codec.Container.MKV,
@ -427,9 +429,11 @@ fun createDeviceProfile(
if (jellyfinTenEleven) add("DOVIInvalid") if (jellyfinTenEleven) add("DOVIInvalid")
if (!supportsHevcDolbyVisionEL) { if (!supportsHevcDolbyVisionEL) {
if (jellyfinTenEleven) { if (!dolbyVisionELDirectPlay) {
add("DOVIWithEL") if (jellyfinTenEleven) {
if (!supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) add("DOVIWithELHDR10Plus") add("DOVIWithEL")
if (!supportsHevcHDR10Plus && !KnownDefects.hevcDoviHdr10PlusBug) add("DOVIWithELHDR10Plus")
}
} }
if (!supportsHevcDolbyVision) { if (!supportsHevcDolbyVision) {

View file

@ -45,6 +45,7 @@ message PlaybackOverrides{
bool direct_play_ass = 3; bool direct_play_ass = 3;
bool direct_play_pgs = 4; bool direct_play_pgs = 4;
MediaExtensionStatus media_extensions_enabled = 5; MediaExtensionStatus media_extensions_enabled = 5;
bool direct_play_dolby_vision_e_l = 6;
} }
message PlaybackPreferences { message PlaybackPreferences {

View file

@ -426,6 +426,8 @@
<string name="dolby_digital">DD</string> <string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string> <string name="dolby_digital_plus">DD+</string>
<string name="dts">DTS</string> <string name="dts">DTS</string>
<string name="force_dovi_profile_7">Direct play Dolby Vision Profile 7</string>
<string name="force_dovi_profile_7_summary">Ignores device compatibility checks</string>
<string name="discover">Discover</string> <string name="discover">Discover</string>
<string name="request">Request</string> <string name="request">Request</string>

View file

@ -92,6 +92,7 @@ androidx-media3-datasource-okhttp = { module = "androidx.media3:media3-datasourc
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" }
androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "androidx-media3" } androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "androidx-media3" }
androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "androidx-media3" } androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "androidx-media3" }
androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "androidx-media3" }
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" } androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" }
androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "androidx-media3" } androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "androidx-media3" }