Improve playback errors UI

This commit is contained in:
vfsfitvnm
2022-08-07 12:41:27 +02:00
parent e4e53bf056
commit 073a50b34c
4 changed files with 181 additions and 102 deletions

View File

@@ -0,0 +1,9 @@
package it.vfsfitvnm.vimusic.service
import androidx.media3.common.PlaybackException
class PlayableFormatNotFoundException : PlaybackException(null, null, ERROR_CODE_REMOTE_ERROR)
class UnplayableException : PlaybackException(null, null, ERROR_CODE_REMOTE_ERROR)
class LoginRequiredException : PlaybackException(null, null, ERROR_CODE_REMOTE_ERROR)

View File

@@ -268,10 +268,17 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
} }
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
maybeRecoverPlaybackError()
maybeNormalizeVolume() maybeNormalizeVolume()
maybeProcessRadio() maybeProcessRadio()
} }
private fun maybeRecoverPlaybackError() {
if (player.playerError != null) {
player.prepare()
}
}
private fun maybeProcessRadio() { private fun maybeProcessRadio() {
radio?.let { radio -> radio?.let { radio ->
if (player.mediaItemCount - player.currentMediaItemIndex <= 3) { if (player.mediaItemCount - player.currentMediaItemIndex <= 3) {
@@ -595,11 +602,9 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
} }
format.url format.url
} ?: throw PlaybackException( } ?: throw PlayableFormatNotFoundException()
"Couldn't find a playable audio format", "UNPLAYABLE" -> throw UnplayableException()
null, "LOGIN_REQUIRED" -> throw LoginRequiredException()
PlaybackException.ERROR_CODE_REMOTE_ERROR
)
else -> throw PlaybackException( else -> throw PlaybackException(
status, status,
null, null,
@@ -614,7 +619,7 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
.subrange(dataSpec.uriPositionOffset, chunkLength) .subrange(dataSpec.uriPositionOffset, chunkLength)
} ?: throw PlaybackException( } ?: throw PlaybackException(
null, null,
null, urlResult?.exceptionOrNull(),
PlaybackException.ERROR_CODE_REMOTE_ERROR PlaybackException.ERROR_CODE_REMOTE_ERROR
) )
} }

View File

@@ -0,0 +1,75 @@
package it.vfsfitvnm.vimusic.ui.views.player
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import it.vfsfitvnm.vimusic.ui.styling.BlackColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.medium
@Composable
fun PlaybackError(
isDisplayed: Boolean,
messageProvider: () -> String,
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
val (_, typography) = LocalAppearance.current
Box {
AnimatedVisibility(
visible = isDisplayed,
enter = fadeIn(),
exit = fadeOut(),
) {
Spacer(
modifier = modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
onDismiss()
}
)
}
.fillMaxSize()
.background(Color.Black.copy(0.8f))
)
}
AnimatedVisibility(
visible = isDisplayed,
enter = slideInVertically { -it },
exit = slideOutVertically { -it },
modifier = Modifier
.align(Alignment.TopCenter)
) {
BasicText(
text = remember { messageProvider() },
style = typography.xs.center.medium.color(BlackColorPalette.text),
modifier = Modifier
.background(Color.Black.copy(0.4f))
.padding(all = 8.dp)
.fillMaxWidth()
)
}
}
}

View File

@@ -11,7 +11,6 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -27,12 +26,16 @@ import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError import it.vfsfitvnm.vimusic.service.LoginRequiredException
import it.vfsfitvnm.vimusic.service.PlayableFormatNotFoundException
import it.vfsfitvnm.vimusic.service.UnplayableException
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.px import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.utils.rememberError import it.vfsfitvnm.vimusic.utils.rememberError
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex
import it.vfsfitvnm.vimusic.utils.thumbnail import it.vfsfitvnm.vimusic.utils.thumbnail
import java.net.UnknownHostException
import java.nio.channels.UnresolvedAddressException
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
@@ -55,105 +58,92 @@ fun Thumbnail(
val error by rememberError(player) val error by rememberError(player)
if (error == null) { AnimatedContent(
AnimatedContent( targetState = mediaItemIndex,
targetState = mediaItemIndex, transitionSpec = {
transitionSpec = { val slideDirection =
val slideDirection = if (targetState > initialState) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right
if (targetState > initialState) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right
(slideIntoContainer(slideDirection) + fadeIn() with (slideIntoContainer(slideDirection) + fadeIn() with
slideOutOfContainer(slideDirection) + fadeOut()).using( slideOutOfContainer(slideDirection) + fadeOut()).using(
SizeTransform(clip = false) SizeTransform(clip = false)
) )
}, },
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = modifier modifier = modifier
.aspectRatio(1f) .aspectRatio(1f)
) { currentMediaItemIndex -> ) { currentMediaItemIndex ->
val mediaItem = remember(currentMediaItemIndex) { val mediaItem = remember(currentMediaItemIndex) {
player.getMediaItemAt(currentMediaItemIndex) player.getMediaItemAt(currentMediaItemIndex)
}
Box(
modifier = Modifier
.clip(ThumbnailRoundness.shape)
.size(thumbnailSizeDp)
) {
AsyncImage(
model = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
onShowLyrics(true)
},
onLongPress = {
onShowStatsForNerds(true)
}
)
}
.fillMaxSize()
)
Lyrics(
mediaId = mediaItem.mediaId,
isDisplayed = isShowingLyrics,
onDismiss = {
onShowLyrics(false)
},
onLyricsUpdate = { areSynchronized, mediaId, lyrics ->
if (areSynchronized) {
if (Database.updateSynchronizedLyrics(mediaId, lyrics) == 0) {
if (mediaId == mediaItem.mediaId) {
Database.insert(mediaItem) { song ->
song.copy(synchronizedLyrics = lyrics)
}
}
}
} else {
if (Database.updateLyrics(mediaId, lyrics) == 0) {
if (mediaId == mediaItem.mediaId) {
Database.insert(mediaItem) { song ->
song.copy(lyrics = lyrics)
}
}
}
}
},
size = thumbnailSizeDp,
mediaMetadataProvider = mediaItem::mediaMetadata,
durationProvider = player::getDuration,
nestedScrollConnectionProvider = nestedScrollConnectionProvider,
)
StatsForNerds(
mediaId = mediaItem.mediaId,
isDisplayed = isShowingStatsForNerds,
onDismiss = {
onShowStatsForNerds(false)
},
modifier = Modifier
)
}
} }
} else {
Box( Box(
contentAlignment = Alignment.Center, modifier = Modifier
modifier = modifier .clip(ThumbnailRoundness.shape)
.padding(bottom = 32.dp)
.padding(horizontal = 32.dp)
.size(thumbnailSizeDp) .size(thumbnailSizeDp)
) { ) {
LoadingOrError( AsyncImage(
errorMessage = error?.javaClass?.canonicalName, model = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
onRetry = { contentDescription = null,
player.playWhenReady = true contentScale = ContentScale.Crop,
player.prepare() modifier = Modifier
} .pointerInput(Unit) {
) {} detectTapGestures(
onTap = { onShowLyrics(true) },
onLongPress = { onShowStatsForNerds(true) }
)
}
.fillMaxSize()
)
Lyrics(
mediaId = mediaItem.mediaId,
isDisplayed = isShowingLyrics && error == null,
onDismiss = { onShowLyrics(false) },
onLyricsUpdate = { areSynchronized, mediaId, lyrics ->
if (areSynchronized) {
if (Database.updateSynchronizedLyrics(mediaId, lyrics) == 0) {
if (mediaId == mediaItem.mediaId) {
Database.insert(mediaItem) { song ->
song.copy(synchronizedLyrics = lyrics)
}
}
}
} else {
if (Database.updateLyrics(mediaId, lyrics) == 0) {
if (mediaId == mediaItem.mediaId) {
Database.insert(mediaItem) { song ->
song.copy(lyrics = lyrics)
}
}
}
}
},
size = thumbnailSizeDp,
mediaMetadataProvider = mediaItem::mediaMetadata,
durationProvider = player::getDuration,
nestedScrollConnectionProvider = nestedScrollConnectionProvider,
)
StatsForNerds(
mediaId = mediaItem.mediaId,
isDisplayed = isShowingStatsForNerds && error == null,
onDismiss = { onShowStatsForNerds(false) }
)
PlaybackError(
isDisplayed = error != null,
messageProvider = {
when (error?.cause?.cause) {
is UnresolvedAddressException, is UnknownHostException -> "A network error has occurred"
is PlayableFormatNotFoundException -> "Couldn't find a playable audio format"
is UnplayableException -> "The original video source of this song has been deleted"
is LoginRequiredException -> "This song cannot be played due to server restrictions"
else -> "An unknown playback error has occurred"
}
},
onDismiss = player::prepare
)
} }
} }
} }