Skip to content

Commit

Permalink
improve
Browse files Browse the repository at this point in the history
  • Loading branch information
mattcarter11 committed Dec 18, 2024
1 parent 7307632 commit 63a937d
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ enum class AudioQuality {

val PersistentQueueKey = booleanPreferencesKey("persistentQueue")
val SkipSilenceKey = booleanPreferencesKey("skipSilence")
val SkipOnErrorKey = booleanPreferencesKey("skipOnError")
val AudioNormalizationKey = booleanPreferencesKey("audioNormalization")
val KeepAliveKey = booleanPreferencesKey("keepAlive")
val StopMusicOnTaskClearKey = booleanPreferencesKey("stopMusicOnTaskClear")
Expand All @@ -55,6 +54,11 @@ val RepeatModeKey = intPreferencesKey("repeatMode")
val LastPosKey = longPreferencesKey("lastPosKey")
val LockQueueKey = booleanPreferencesKey("lockQueue")
val minPlaybackDurKey = intPreferencesKey("minPlaybackDur")
val PlayerOnErrorActionKey = stringPreferencesKey("PlayerOnError")

enum class PlayerOnError {
PAUSE, SKIP, WAIT_TO_RECONNECT
}


/**
Expand Down
77 changes: 36 additions & 41 deletions app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleRepeatM
import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleShuffle
import com.dd3boh.outertune.constants.PauseListenHistoryKey
import com.dd3boh.outertune.constants.PersistentQueueKey
import com.dd3boh.outertune.constants.PlayerOnError
import com.dd3boh.outertune.constants.PlayerOnErrorActionKey
import com.dd3boh.outertune.constants.PlayerVolumeKey
import com.dd3boh.outertune.constants.RepeatModeKey
import com.dd3boh.outertune.constants.ShowLyricsKey
import com.dd3boh.outertune.constants.SkipOnErrorKey
import com.dd3boh.outertune.constants.SkipSilenceKey
import com.dd3boh.outertune.constants.minPlaybackDurKey
import com.dd3boh.outertune.db.MusicDatabase
Expand Down Expand Up @@ -107,8 +108,6 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
Expand All @@ -132,8 +131,6 @@ import javax.inject.Inject
import kotlin.math.min
import kotlin.math.pow

const val MAX_CONSECUTIVE_ERR = 3

@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@AndroidEntryPoint
class MusicService : MediaLibraryService(),
Expand All @@ -157,6 +154,8 @@ class MusicService : MediaLibraryService(),
private lateinit var connectivityManager: ConnectivityManager

private val audioQuality by enumPreference(this, AudioQualityKey, AudioQuality.AUTO)
private val playerOnErrorAction by enumPreference(this, PlayerOnErrorActionKey, PlayerOnError.WAIT_TO_RECONNECT)


var queueTitle: String? = null
var queuePlaylistId: String? = null
Expand Down Expand Up @@ -189,16 +188,14 @@ class MusicService : MediaLibraryService(),

private var isAudioEffectSessionOpened = false

var consecutivePlaybackErr = 0

override fun onCreate() {
super.onCreate()
val notificationBuilder = NotificationCompat.Builder(this, KEEP_ALIVE_CHANNEL_ID)
.setContentTitle(getString(R.string.music_player))
.setSmallIcon(R.drawable.small_icon)
.setOngoing(true) // Ensures notification stays until service stops

val notification = notificationBuilder.build()
notificationBuilder.build()

// FG notification
if (dataStore.get(KeepAliveKey, false)) {
Expand Down Expand Up @@ -250,56 +247,54 @@ class MusicService : MediaLibraryService(),
addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
if (!dataStore.get(SkipOnErrorKey, true)) {

if (playerOnErrorAction == PlayerOnError.SKIP) {
player.seekTo(player.nextMediaItemIndex, C.TIME_UNSET)
player.prepare()
player.play()
Toast.makeText(
this@MusicService,
getString(R.string.play_next) + " " + getString(R.string.on_error).lowercase(),
Toast.LENGTH_LONG
).show()
return
}

consecutivePlaybackErr += 2

// If connection lost, stop playing
val noConnectionError = (error.cause?.cause is PlaybackException) && (error.cause?.cause as PlaybackException).errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED
if (!isNetworkConnected.value || noConnectionError) {
if (playerOnErrorAction == PlayerOnError.PAUSE) {
player.pause()
waitingForNetworkConnection.value = true
Toast.makeText(this@MusicService, getString(R.string.error_no_internet), Toast.LENGTH_SHORT).show()
Toast.makeText(
this@MusicService,
getString(R.string.pause) + " " + getString(R.string.on_error).lowercase(),
Toast.LENGTH_LONG
).show()
return
}

if (playerOnErrorAction == PlayerOnError.WAIT_TO_RECONNECT){
val noConnectionError = (error.cause?.cause is PlaybackException) && (error.cause?.cause as PlaybackException).errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED
if (!isNetworkConnected.value || noConnectionError) {
player.pause()
waitingForNetworkConnection.value = true
Toast.makeText(
this@MusicService,
getString(R.string.wait_to_reconnect) + " " + getString(R.string.on_error).lowercase(),
Toast.LENGTH_LONG
).show()
return
}
}

Toast.makeText(
this@MusicService,
"Playback error: ${error.message} (${error.errorCode}): ${error.cause?.message?: "No further errors."} ",
Toast.LENGTH_SHORT
).show()

/**
* Auto skip to the next media item on error.
*
* To prevent a "runaway diesel engine" scenario, force the user to take action after
* too many errors come up too quickly. Pause to show player "stopped" state
*/
val nextWindowIndex = player.nextMediaItemIndex
if (consecutivePlaybackErr <= MAX_CONSECUTIVE_ERR && nextWindowIndex != C.INDEX_UNSET) {
player.seekTo(nextWindowIndex, C.TIME_UNSET)
player.prepare()
player.play()
} else {
player.pause()
Toast.makeText(
this@MusicService,
"Playback stopped due to too many errors",
Toast.LENGTH_SHORT
).show()
consecutivePlaybackErr = 0
}
player.pause()
}

// start playback again on seek
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
// +2 when and error happens, and -1 when transition. Thus when error, number increments by 1, else doesn't change
if (consecutivePlaybackErr > 0) {
consecutivePlaybackErr --
}

if (player.isPlaying && reason == MEDIA_ITEM_TRANSITION_REASON_SEEK) {
player.prepare()
Expand Down
45 changes: 20 additions & 25 deletions app/src/main/java/com/dd3boh/outertune/ui/component/Items.kt
Original file line number Diff line number Diff line change
Expand Up @@ -224,17 +224,18 @@ fun ListItem(
trailingContent: @Composable RowScope.() -> Unit = {},
isSelected: Boolean? = false,
isActive: Boolean = false,
isLocalSong: Boolean? = null,
isLocalSong: Boolean = false,
isLiked: Boolean = false,
inLibrary: Boolean = false,
enabled: Boolean = true,
) = ListItem(
title = title,
subtitle = {
badges()

// local song indicator
if (isLocalSong == true) {
FolderCopy()
}
if (isLiked)Icon.Favorite()
if (inLibrary) Icon.Library()
if (isLocalSong) FolderCopy()

if (!subtitle.isNullOrEmpty()) {
Text(
Expand Down Expand Up @@ -346,25 +347,8 @@ fun SongListItem(
showLikedIcon: Boolean = true,
showInLibraryIcon: Boolean = false,
showDownloadIcon: Boolean = true,
showLocalIcon: Boolean = true,
isSelected: Boolean = false,
badges: @Composable RowScope.() -> Unit = {
if (showLikedIcon && song.song.liked) {
Icon.Favorite()
}
if (showInLibraryIcon && song.song.inLibrary != null) {
Icon.Library()
}
if (showDownloadIcon) {
val download by LocalDownloadUtil.current.getDownload(song.id)
.collectAsState(initial = null)
Icon.Download(download?.state)
}

// local song indicator
if (song.song.isLocal) {
FolderCopy()
}
},
isActive: Boolean = false,
isPlaying: Boolean = false,
trailingContent: @Composable RowScope.() -> Unit = {},
Expand All @@ -377,7 +361,13 @@ fun SongListItem(
song.artists.joinToString { it.name },
makeTimeString(song.song.duration * 1000L)
),
badges = badges,
badges = {
if (showDownloadIcon) {
val download by LocalDownloadUtil.current.getDownload(song.id)
.collectAsState(initial = null)
Icon.Download(download?.state)
}
},
thumbnailContent = {
ItemThumbnail(
thumbnailUrl = if (song.song.isLocal) song.song.localPath else song.song.thumbnailUrl,
Expand All @@ -392,6 +382,9 @@ fun SongListItem(
modifier = modifier,
isSelected = isSelected,
isActive = isActive,
isLocalSong = showLocalIcon && song.song.isLocal,
isLiked = showLikedIcon && song.song.liked,
inLibrary = showInLibraryIcon && song.song.inLibrary != null,
enabled = song.song.isAvailableOffline() || isNetworkConnected
)
}
Expand Down Expand Up @@ -989,7 +982,9 @@ fun MediaMetadataListItem(
modifier = modifier,
isSelected = isSelected,
isActive = isActive,
isLocalSong = mediaMetadata.isLocal
isLocalSong = mediaMetadata.isLocal,
isLiked = mediaMetadata.liked,
inLibrary = mediaMetadata.inLibrary != null
)

@Composable
Expand Down
54 changes: 45 additions & 9 deletions app/src/main/java/com/dd3boh/outertune/ui/menu/PlayerMenu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ import androidx.compose.material.icons.automirrored.rounded.PlaylistAdd
import androidx.compose.material.icons.automirrored.rounded.QueueMusic
import androidx.compose.material.icons.automirrored.rounded.VolumeUp
import androidx.compose.material.icons.rounded.AddCircleOutline
import androidx.compose.material.icons.rounded.CloudSync
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.LibraryAdd
import androidx.compose.material.icons.rounded.LibraryAddCheck
import androidx.compose.material.icons.rounded.Pause
import androidx.compose.material.icons.rounded.Radio
import androidx.compose.material.icons.rounded.RemoveCircleOutline
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material.icons.rounded.SkipNext
import androidx.compose.material.icons.rounded.SlowMotionVideo
import androidx.compose.material.icons.rounded.Timer
import androidx.compose.material.icons.rounded.Tune
Expand Down Expand Up @@ -77,6 +80,8 @@ import com.dd3boh.outertune.LocalDownloadUtil
import com.dd3boh.outertune.LocalPlayerConnection
import com.dd3boh.outertune.R
import com.dd3boh.outertune.constants.ListItemHeight
import com.dd3boh.outertune.constants.PlayerOnError
import com.dd3boh.outertune.constants.PlayerOnErrorActionKey
import com.dd3boh.outertune.models.MediaMetadata
import com.dd3boh.outertune.playback.ExoDownloadService
import com.dd3boh.outertune.playback.PlayerConnection.Companion.queueBoard
Expand All @@ -87,6 +92,7 @@ import com.dd3boh.outertune.ui.component.GridMenu
import com.dd3boh.outertune.ui.component.GridMenuItem
import com.dd3boh.outertune.ui.component.ListDialog
import com.dd3boh.outertune.ui.component.SleepTimerGridMenu
import com.dd3boh.outertune.utils.rememberEnumPreference
import com.zionhuang.innertube.YouTube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
Expand Down Expand Up @@ -117,9 +123,11 @@ fun PlayerMenu(
val currentPlayCount by playerConnection.currentPlayCount.collectAsState(initial = null)
val librarySong by database.song(mediaMetadata.id).collectAsState(initial = null)
val coroutineScope = rememberCoroutineScope()

val (playerOnErrorAction, onPlayerOnErrorAction) = rememberEnumPreference(key = PlayerOnErrorActionKey, defaultValue = PlayerOnError.WAIT_TO_RECONNECT)
val download by LocalDownloadUtil.current.getDownload(mediaMetadata.id).collectAsState(initial = null)

var toast by rememberSaveable { mutableStateOf<Toast?>(null) }

var showChooseQueueDialog by rememberSaveable {
mutableStateOf(false)
}
Expand Down Expand Up @@ -319,11 +327,11 @@ fun PlayerMenu(
.verticalScroll(rememberScrollState())
) {
listOf(
stringResource(R.string.song_title) to mediaMetadata?.title,
stringResource(R.string.song_artists) to mediaMetadata?.artists?.joinToString { it.name },
stringResource(R.string.sort_by_date_released) to mediaMetadata?.getDateString(),
stringResource(R.string.sort_by_date_modified) to mediaMetadata?.getDateModifiedString(),
stringResource(R.string.media_id) to mediaMetadata?.id,
stringResource(R.string.song_title) to mediaMetadata.title,
stringResource(R.string.song_artists) to mediaMetadata.artists.joinToString { it.name },
stringResource(R.string.sort_by_date_released) to mediaMetadata.getDateString(),
stringResource(R.string.sort_by_date_modified) to mediaMetadata.getDateModifiedString(),
stringResource(R.string.media_id) to mediaMetadata.id,
stringResource(R.string.play_count) to currentPlayCount.toString(),
"Itag" to currentFormat?.itag?.toString(),
stringResource(R.string.mime_type) to currentFormat?.mimeType,
Expand Down Expand Up @@ -387,7 +395,7 @@ fun PlayerMenu(
bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()
)
) {
if (mediaMetadata.isLocal != true)
if (!mediaMetadata.isLocal)
GridMenuItem(
icon = Icons.Rounded.Radio,
title = R.string.start_radio
Expand All @@ -407,7 +415,7 @@ fun PlayerMenu(
) {
showChoosePlaylistDialog = true
}
if (mediaMetadata.isLocal != true)
if (!mediaMetadata.isLocal)
DownloadGridMenu(
state = download?.state,
onDownload = {
Expand Down Expand Up @@ -467,7 +475,7 @@ fun PlayerMenu(
}
}

if (mediaMetadata.isLocal != true)
if (!mediaMetadata.isLocal)
GridMenuItem(
icon = Icons.Rounded.Share,
title = R.string.share
Expand All @@ -480,25 +488,53 @@ fun PlayerMenu(
context.startActivity(Intent.createChooser(intent, null))
onDismiss()
}

GridMenuItem(
icon = Icons.Rounded.Info,
title = R.string.details
) {
showDetailsDialog = true
}

SleepTimerGridMenu(
sleepTimerTimeLeft = sleepTimerTimeLeft,
enabled = sleepTimerEnabled
) {
if (sleepTimerEnabled) playerConnection.service.sleepTimer.clear()
else showSleepTimerDialog = true
}

GridMenuItem(
icon = Icons.Rounded.Tune,
title = R.string.advanced
) {
showPitchTempoDialog = true
}

GridMenuItem(
icon = when (playerOnErrorAction) {
PlayerOnError.PAUSE -> Icons.Rounded.Pause
PlayerOnError.SKIP -> Icons.Rounded.SkipNext
PlayerOnError.WAIT_TO_RECONNECT -> Icons.Rounded.CloudSync
},
title = R.string.on_error
) {
val nextState = when (playerOnErrorAction) {
PlayerOnError.PAUSE -> PlayerOnError.SKIP
PlayerOnError.SKIP -> PlayerOnError.WAIT_TO_RECONNECT
PlayerOnError.WAIT_TO_RECONNECT -> PlayerOnError.PAUSE
}

toast?.cancel()
toast = when (nextState) {
PlayerOnError.PAUSE -> Toast.makeText(context, R.string.pause, Toast.LENGTH_SHORT)
PlayerOnError.SKIP -> Toast.makeText(context, R.string.play_next, Toast.LENGTH_SHORT)
PlayerOnError.WAIT_TO_RECONNECT -> Toast.makeText(context, R.string.wait_to_reconnect, Toast.LENGTH_SHORT)
}
toast?.show()

onPlayerOnErrorAction(nextState)
}
}
}

Expand Down
Loading

0 comments on commit 63a937d

Please sign in to comment.