Few UI fixes especially backgrounding the app (#39)

Ensures playback (both theme song & video) is stopped when the app is
backgrounded, like hitting the home button.

Fixes a UI glitch when marking a series as played.
This commit is contained in:
damontecres 2025-10-19 17:24:08 -04:00 committed by GitHub
parent 63baf7f904
commit c3cc284238
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 143 additions and 52 deletions

View file

@ -37,7 +37,6 @@ 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.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.ThemeSongPlayer
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
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -69,9 +68,6 @@ class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var appUpgradeHandler: AppUpgradeHandler lateinit var appUpgradeHandler: AppUpgradeHandler
@Inject
lateinit var themeSongPlayer: ThemeSongPlayer
@OptIn(ExperimentalTvMaterial3Api::class) @OptIn(ExperimentalTvMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -168,9 +164,4 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
override fun onStop() {
super.onStop()
themeSongPlayer.stop()
}
} }

View file

@ -26,6 +26,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.media3.common.Player import androidx.media3.common.Player
import coil3.request.ErrorResult import coil3.request.ErrorResult
import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumn
@ -360,3 +361,8 @@ fun CoroutineScope.launchIO(
* *
*/ */
fun UUID.toServerString() = this.toString().replace("-", "") fun UUID.toServerString() = this.toString().replace("-", "")
suspend fun <T> MutableLiveData<T>.setValueOnMain(value: T) =
withContext(Dispatchers.Main) {
this@setValueOnMain.value = value
}

View file

@ -36,6 +36,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
@ -82,6 +83,15 @@ fun SeriesDetails(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SeriesViewModel = hiltViewModel(), viewModel: SeriesViewModel = hiltViewModel(),
) { ) {
LifecycleStartEffect(destination.itemId) {
viewModel.maybePlayThemeSong(
destination.itemId,
preferences.appPreferences.interfacePreferences.playThemeSongs,
)
onStopOrDispose {
viewModel.release()
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.init(preferences, destination.itemId, destination.item, null, null) viewModel.init(preferences, destination.itemId, destination.item, null, null)
} }

View file

@ -17,6 +17,7 @@ import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination 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.setValueOnMain
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
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
@ -101,7 +102,6 @@ class SeriesViewModel
people.map { Person.fromDto(it, api) } people.map { Person.fromDto(it, api) }
}.orEmpty() }.orEmpty()
} }
maybePlayThemeSong(prefs.appPreferences.interfacePreferences.playThemeSongs)
} }
} }
@ -109,7 +109,10 @@ class SeriesViewModel
* If the series has a theme song & app settings allow, play it * If the series has a theme song & app settings allow, play it
*/ */
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun maybePlayThemeSong(playThemeSongs: ThemeSongVolume) { fun maybePlayThemeSong(
seriesId: UUID,
playThemeSongs: ThemeSongVolume,
) {
viewModelScope.launch(ExceptionHandler()) { viewModelScope.launch(ExceptionHandler()) {
val themeSongs = api.libraryApi.getThemeSongs(seriesId).content val themeSongs = api.libraryApi.getThemeSongs(seriesId).content
themeSongs.items.firstOrNull()?.let { theme -> themeSongs.items.firstOrNull()?.let { theme ->
@ -141,10 +144,10 @@ class SeriesViewModel
themeSongPlayer.stop() themeSongPlayer.stop()
} }
private suspend fun getSeasons(item: BaseItem): ItemListAndMapping { private suspend fun getSeasons(series: BaseItem): ItemListAndMapping {
val request = val request =
GetItemsRequest( GetItemsRequest(
parentId = item.id, parentId = series.id,
recursive = false, recursive = false,
includeItemTypes = listOf(BaseItemKind.SEASON), includeItemTypes = listOf(BaseItemKind.SEASON),
sortBy = listOf(ItemSortBy.INDEX_NUMBER), sortBy = listOf(ItemSortBy.INDEX_NUMBER),
@ -164,7 +167,7 @@ class SeriesViewModel
viewModelScope, viewModelScope,
) )
pager.init() pager.init()
Timber.Forest.v("Loaded ${pager.size} seasons for series ${item.id}") Timber.Forest.v("Loaded ${pager.size} seasons for series ${series.id}")
val pairs = val pairs =
pager.mapIndexed { index, _ -> pager.mapIndexed { index, _ ->
val season = pager.getBlocking(index) val season = pager.getBlocking(index)
@ -235,7 +238,9 @@ class SeriesViewModel
} else { } else {
api.playStateApi.markUnplayedItem(seriesId) api.playStateApi.markUnplayedItem(seriesId)
} }
fetchItem(seriesId, null) val series = fetchItem(seriesId, null)
val seasons = getSeasons(series)
this@SeriesViewModel.seasons.setValueOnMain(seasons)
} }
fun refreshEpisode( fun refreshEpisode(

View file

@ -12,6 +12,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleStartEffect
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.chooseSource import com.github.damontecres.wholphin.data.model.chooseSource
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
@ -59,6 +60,15 @@ fun SeriesOverview(
viewModel: SeriesViewModel = hiltViewModel(), viewModel: SeriesViewModel = hiltViewModel(),
initialSeasonEpisode: SeasonEpisode? = null, initialSeasonEpisode: SeasonEpisode? = null,
) { ) {
LifecycleStartEffect(destination.itemId) {
viewModel.maybePlayThemeSong(
destination.itemId,
preferences.appPreferences.interfacePreferences.playThemeSongs,
)
onStopOrDispose {
viewModel.release()
}
}
val firstItemFocusRequester = remember { FocusRequester() } val firstItemFocusRequester = remember { FocusRequester() }
val episodeRowFocusRequester = remember { FocusRequester() } val episodeRowFocusRequester = remember { FocusRequester() }

View file

@ -289,7 +289,10 @@ fun SeriesOverviewContent(
chosenStreams = chosenStreams, chosenStreams = chosenStreams,
playOnClick = playOnClick, playOnClick = playOnClick,
moreOnClick = moreOnClick, moreOnClick = moreOnClick,
watchOnClick = watchOnClick, watchOnClick = {
watchOnClick.invoke()
episodeRowFocusRequester.tryRequestFocus()
},
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -43,6 +44,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.cards.BannerCard
import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingPage
@ -71,11 +73,14 @@ fun HomePage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: HomeViewModel = hiltViewModel(), viewModel: HomeViewModel = hiltViewModel(),
) { ) {
var firstLoad by rememberSaveable { mutableStateOf(true) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.init(preferences) viewModel.init(preferences).join()
firstLoad = false
} }
val loading by viewModel.loadingState.observeAsState(LoadingState.Loading) val loading by viewModel.loadingState.observeAsState(LoadingState.Loading)
when (val state = loading) { val homeRows by viewModel.homeRows.observeAsState(listOf())
when (val state = if (firstLoad) loading else LoadingState.Success) {
is LoadingState.Error -> ErrorMessage(state) is LoadingState.Error -> ErrorMessage(state)
LoadingState.Loading, LoadingState.Loading,
@ -83,13 +88,13 @@ fun HomePage(
-> LoadingPage() -> LoadingPage()
LoadingState.Success -> { LoadingState.Success -> {
val homeRows by viewModel.homeRows.observeAsState(listOf())
HomePageContent( HomePageContent(
homeRows, homeRows,
onClickItem = { onClickItem = {
viewModel.navigationManager.navigateTo(it.destination()) viewModel.navigationManager.navigateTo(it.destination())
}, },
modifier, loadingState = loading,
modifier = modifier,
) )
} }
} }
@ -101,6 +106,7 @@ fun HomePageContent(
onClickItem: (BaseItem) -> Unit, onClickItem: (BaseItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onFocusPosition: ((RowColumn) -> Unit)? = null, onFocusPosition: ((RowColumn) -> Unit)? = null,
loadingState: LoadingState? = null,
) { ) {
var position by rememberSaveable(stateSaver = RowColumnSaver) { var position by rememberSaveable(stateSaver = RowColumnSaver) {
mutableStateOf(RowColumn(0, 0)) mutableStateOf(RowColumn(0, 0))
@ -219,6 +225,22 @@ fun HomePageContent(
} }
} }
} }
Box(
modifier =
Modifier
.padding(8.dp)
.size(24.dp)
.align(Alignment.BottomEnd),
) {
when (loadingState) {
LoadingState.Pending,
LoadingState.Loading,
->
CircularProgress(Modifier.fillMaxSize())
else -> {}
}
}
} }
} }

View file

@ -13,6 +13,7 @@ import com.github.damontecres.wholphin.util.supportItemKinds
import com.github.damontecres.wholphin.util.supportedCollectionTypes import com.github.damontecres.wholphin.util.supportedCollectionTypes
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -36,13 +37,14 @@ class HomeViewModel
val api: ApiClient, val api: ApiClient,
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
) : ViewModel() { ) : ViewModel() {
val loadingState = MutableLiveData<LoadingState>(LoadingState.Loading) val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
val homeRows = MutableLiveData<List<HomeRow>>() val homeRows = MutableLiveData<List<HomeRow>>()
fun init(preferences: UserPreferences) { fun init(preferences: UserPreferences): Job {
loadingState.value = LoadingState.Loading
val prefs = preferences.appPreferences.homePagePreferences val prefs = preferences.appPreferences.homePagePreferences
val limit = prefs.maxItemsPerRow val limit = prefs.maxItemsPerRow
viewModelScope.launch( return viewModelScope.launch(
Dispatchers.IO + Dispatchers.IO +
LoadingExceptionHandler( LoadingExceptionHandler(
loadingState, loadingState,

View file

@ -41,6 +41,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.text.Cue import androidx.media3.common.text.Cue
import androidx.media3.common.text.CueGroup import androidx.media3.common.text.CueGroup
@ -85,6 +86,11 @@ fun PlaybackPage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: PlaybackViewModel = hiltViewModel(), viewModel: PlaybackViewModel = hiltViewModel(),
) { ) {
LifecycleStartEffect(destination.itemId) {
onStopOrDispose {
viewModel.release()
}
}
LaunchedEffect(destination.itemId) { LaunchedEffect(destination.itemId) {
viewModel.init(destination, deviceProfile, preferences) viewModel.init(destination, deviceProfile, preferences)
} }

View file

@ -15,7 +15,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.ExoPlayer
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
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
@ -38,6 +37,7 @@ 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
@ -97,6 +97,7 @@ class PlaybackViewModel
constructor( constructor(
@param:ApplicationContext val context: Context, @param:ApplicationContext val context: Context,
val api: ApiClient, val api: ApiClient,
val playerFactory: PlayerFactory,
val playlistCreator: PlaylistCreator, val playlistCreator: PlaylistCreator,
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
val itemPlaybackDao: ItemPlaybackDao, val itemPlaybackDao: ItemPlaybackDao,
@ -104,13 +105,13 @@ class PlaybackViewModel
val itemPlaybackRepository: ItemPlaybackRepository, val itemPlaybackRepository: ItemPlaybackRepository,
) : ViewModel(), ) : ViewModel(),
Player.Listener { Player.Listener {
val player: ExoPlayer = val player by lazy {
ExoPlayer playerFactory
.Builder(context) .create(false)
.build()
.apply { .apply {
playWhenReady = true playWhenReady = true
} }
}
val loading = MutableLiveData<LoadingState>(LoadingState.Loading) val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
@ -711,6 +712,11 @@ class PlaybackViewModel
loading.value = LoadingState.Error("Error during playback", error) loading.value = LoadingState.Error("Error during playback", error)
} }
} }
fun release() {
playerFactory.release()
navigationManager.goBack()
}
} }
data class CurrentPlayback( data class CurrentPlayback(

View file

@ -0,0 +1,49 @@
package com.github.damontecres.wholphin.util
import android.content.Context
import androidx.annotation.OptIn
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import com.github.damontecres.wholphin.hilt.AuthOkHttpClient
import com.github.damontecres.wholphin.hilt.StandardOkHttpClient
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.OkHttpClient
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PlayerFactory
@Inject
constructor(
@param:ApplicationContext private val context: Context,
@param:StandardOkHttpClient private val standardOkHttpClient: OkHttpClient,
@param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient,
) {
private var player: Player? = null
@OptIn(UnstableApi::class)
fun create(useAuth: Boolean): Player {
release()
val player =
ExoPlayer
.Builder(context)
.setMediaSourceFactory(
DefaultMediaSourceFactory(
OkHttpDataSource.Factory(if (useAuth) authOkHttpClient else standardOkHttpClient),
),
).build()
this.player = player
return player
}
fun release() {
player?.let {
it.stop()
it.release()
}
player = null
}
}

View file

@ -1,17 +1,9 @@
package com.github.damontecres.wholphin.util package com.github.damontecres.wholphin.util
import android.content.Context
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import com.github.damontecres.wholphin.hilt.AuthOkHttpClient
import com.github.damontecres.wholphin.preferences.ThemeSongVolume import com.github.damontecres.wholphin.preferences.ThemeSongVolume
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.OkHttpClient
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -22,11 +14,8 @@ import javax.inject.Singleton
class ThemeSongPlayer class ThemeSongPlayer
@Inject @Inject
constructor( constructor(
@param:ApplicationContext val context: Context, val playerFactory: PlayerFactory,
@param:AuthOkHttpClient val okHttpClient: OkHttpClient,
) { ) {
private var player: Player? = null
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun play( fun play(
volume: ThemeSongVolume, volume: ThemeSongVolume,
@ -46,25 +35,17 @@ class ThemeSongPlayer
ThemeSongVolume.HIGHEST -> 75f ThemeSongVolume.HIGHEST -> 75f
} }
val player = val player =
ExoPlayer playerFactory
.Builder(context) .create(true)
.setMediaSourceFactory(
DefaultMediaSourceFactory(
OkHttpDataSource.Factory(okHttpClient),
),
).build()
.apply { .apply {
this.volume = volumeLevel this.volume = volumeLevel
playWhenReady = true playWhenReady = true
} }
player.setMediaItem(MediaItem.fromUri(url)) player.setMediaItem(MediaItem.fromUri(url))
player.prepare() player.prepare()
this.player = player
} }
fun stop() { fun stop() {
player?.stop() playerFactory.release()
player?.release()
player = null
} }
} }

View file

@ -129,7 +129,7 @@ class TrackActivityPlaybackListener(
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
(if (position >= 0) position else player.currentPosition).milliseconds (if (position >= 0) position else player.currentPosition).milliseconds
} }
// Timber.v("saveActivity: itemId=$itemId, pos=$calcPosition") Timber.v("saveActivity: itemId=${itemPlayback.itemId}, pos=$calcPosition")
api.playStateApi.reportPlaybackProgress( api.playStateApi.reportPlaybackProgress(
PlaybackProgressInfo( PlaybackProgressInfo(
itemId = itemPlayback.itemId, itemId = itemPlayback.itemId,