Skip to content

Commit

Permalink
Fix replaying of previous song
Browse files Browse the repository at this point in the history
  • Loading branch information
brahmkshatriya committed Nov 10, 2024
1 parent b2320c8 commit d87690c
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 83 deletions.
2 changes: 1 addition & 1 deletion app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class PlayerService : MediaLibraryService() {
lateinit var current: MutableStateFlow<Current?>

@Inject
lateinit var currentSources: MutableStateFlow<Streamable.Media.Sources?>
lateinit var currentSources: MutableStateFlow<Map<String, Streamable.Media.Sources>>

@Inject
lateinit var fftAudioProcessor: FFTAudioProcessor
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/dev/brahmkshatriya/echo/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class AppModule {

@Provides
@Singleton
fun provideCurrentSourcesFlow() = MutableStateFlow<Streamable.Media.Sources?>(null)
fun provideCurrentSourcesFlow() = MutableStateFlow(mapOf<String, Streamable.Media.Sources>())

@Provides
@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ object MediaStoreUtils {
}.toString().ifEmpty { null }
val liked = likedAudios.contains(id)
val song = Track(
id = "offline:$id",
id = id.toString(),
title = title,
artists = artists,
album = album,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,8 +419,7 @@ class OfflineExtension(

override suspend fun likeTrack(track: Track, isLiked: Boolean) {
val playlist = library.likedPlaylist.id
val id = track.id.substringAfter("offline:").toLong()
if (isLiked) context.addSongToPlaylist(playlist, id, 0)
if (isLiked) context.addSongToPlaylist(playlist, track.id.toLong(), 0)
else {
val index = library.likedPlaylist.songList.indexOfFirst { it.id == track.id }
context.removeSongFromPlaylist(playlist, index)
Expand Down Expand Up @@ -449,8 +448,7 @@ class OfflineExtension(
playlist: Playlist, tracks: List<Track>, index: Int, new: List<Track>
) {
new.forEach {
val id = it.id.substringAfter("offline:").toLong()
context.addSongToPlaylist(playlist.id.toLong(), id, index)
context.addSongToPlaylist(playlist.id.toLong(), it.id.toLong(), index)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ class TestExtension : ExtensionClient, LoginClient.UsernamePassword, TrackClient
}

private val radio = Radio("empty", "empty")
override fun loadTracks(radio: Radio) = PagedData.Single<Track> { emptyList() }
override fun loadTracks(radio: Radio) = PagedData.Single {
listOf(
(Srcs.Merged.createTrack().media as EchoMediaItem.TrackItem).track,
)
}

override suspend fun radio(track: Track, context: EchoMediaItem?) = radio
override suspend fun radio(album: Album) = radio
override suspend fun radio(artist: Artist) = radio
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ object MediaItemUtils {
item.build()
}

fun buildExternal(
fun buildWithBackgroundAndSubtitle(
mediaItem: MediaItem,
background: Streamable.Media.Background?,
subtitle: Streamable.Media.Subtitle?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import dev.brahmkshatriya.echo.playback.MediaItemUtils.isLoaded
import dev.brahmkshatriya.echo.playback.MediaItemUtils.sourcesIndex
import dev.brahmkshatriya.echo.playback.MediaItemUtils.subtitleIndex
import dev.brahmkshatriya.echo.playback.MediaItemUtils.track
import dev.brahmkshatriya.echo.ui.exception.AppException.Companion.toAppException
import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel.Companion.noClient
import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel.Companion.trackNotSupported
import kotlinx.coroutines.Dispatchers
Expand All @@ -33,14 +32,16 @@ class StreamableLoader(
) {
suspend fun load(mediaItem: MediaItem) = withContext(Dispatchers.IO) {
extensionListFlow.first { it != null }
val new = if (!mediaItem.isLoaded) mediaItem
val new = if (mediaItem.isLoaded) mediaItem
else MediaItemUtils.buildLoaded(settings, mediaItem, loadTrack(mediaItem))

val srcs = async { loadSources(new) }
val background = async { if (new.backgroundIndex < 0) null else loadBackground(new) }
val subtitle = async { if (new.subtitleIndex < 0) null else loadSubtitle(new) }

MediaItemUtils.buildExternal(new, background.await(), subtitle.await()) to srcs.await()
MediaItemUtils.buildWithBackgroundAndSubtitle(
new, background.await(), subtitle.await()
) to srcs.await()
}

private suspend fun <T> withClient(
Expand All @@ -52,7 +53,7 @@ class StreamableLoader(
val client = extension.instance.value.getOrNull()
if (client !is TrackClient)
throw Exception(context.trackNotSupported(extension.metadata.name).message)
return runCatching { block(client) }.getOrElse { throw it.toAppException(extension) }
return block(client)
}

private suspend fun loadTrack(item: MediaItem) = withClient(item) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.datasource.ResolvingDataSource
import androidx.media3.datasource.TransferListener
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider
Expand Down Expand Up @@ -39,59 +40,71 @@ import kotlinx.coroutines.launch

@UnstableApi
class StreamableMediaSource(
private var mediaItem: MediaItem,
private val factory: Factory,
private val context: Context,
private val scope: CoroutineScope,
private val current: MutableStateFlow<Map<String, Streamable.Media.Sources>>,
private val loader: StreamableLoader,
private val dash: Lazy<MediaSource.Factory>,
private val hls: Lazy<MediaSource.Factory>,
private val default: Lazy<MediaSource.Factory>,
private var mediaItem: MediaItem
) : CompositeMediaSource<Nothing>() {

fun create(mediaItem: MediaItem, index: Int, source: Streamable.Source): MediaSource {
val type = (source as? Streamable.Source.Http)?.type
val factory = when (type) {
Streamable.SourceType.DASH -> dash
Streamable.SourceType.HLS -> hls
Streamable.SourceType.Progressive, null -> default
}
val new = MediaItemUtils.buildForSource(mediaItem, index, source)
return factory.value.createMediaSource(new)
}

private var error: Throwable? = null
override fun maybeThrowSourceInfoRefreshError() {
error?.let { throw it }
super.maybeThrowSourceInfoRefreshError()
}

private val context = factory.context
private val scope = factory.scope
private val current = factory.current
private val loader = factory.loader

private lateinit var actualSource: MediaSource
override fun prepareSourceInternal(mediaTransferListener: TransferListener?) {
super.prepareSourceInternal(mediaTransferListener)
val handler = Util.createHandlerForCurrentLooper()
scope.launch {
val (item, sources) = runCatching { loader.load(mediaItem) }.getOrElse {
val (new, streamable) = runCatching { loader.load(mediaItem) }.getOrElse {
error = it
return@launch
}
onResolved(item, sources)
handler.post { prepareChildSource(null, actualSource) }
}
}
mediaItem = new
current.apply {
value = value.toMutableMap().apply { set(new.mediaId, streamable) }
}

private lateinit var actualSource: MediaSource
private fun onResolved(
new: MediaItem, streamable: Streamable.Media.Sources,
) {
mediaItem = new
current.value = streamable
if (new.clientId != OfflineExtension.metadata.id) {
val track = mediaItem.track
context.saveToCache(track.id, new.clientId to track, "track")
}
val sources = streamable.sources
actualSource = when (sources.size) {
0 -> throw Exception(context.getString(R.string.streamable_not_found))
1 -> factory.create(new, 0, sources.first())
else -> {
if (streamable.merged) MergingMediaSource(
*sources.mapIndexed { index, source ->
factory.create(new, index, source)
}.toTypedArray()
) else {
val index = mediaItem.sourceIndex
val source = sources[index]
factory.create(new, index, source)
if (new.clientId != OfflineExtension.metadata.id) {
val track = mediaItem.track
context.saveToCache(track.id, new.clientId to track, "track")
}

val sources = streamable.sources
actualSource = when (sources.size) {
0 -> throw Exception(context.getString(R.string.streamable_not_found))
1 -> create(new, 0, sources.first())
else -> {
if (streamable.merged) MergingMediaSource(
*sources.mapIndexed { index, source ->
create(new, index, source)
}.toTypedArray()
) else {
val index = mediaItem.sourceIndex
val source = sources[index]
create(new, index, source)
}
}
}
handler.post {
runCatching { prepareChildSource(null, actualSource) }
}
}
}

Expand Down Expand Up @@ -123,26 +136,23 @@ class StreamableMediaSource(
actualSource.updateMediaItem(mediaItem)
}


@UnstableApi
class Factory(
val context: Context,
val scope: CoroutineScope,
val current: MutableStateFlow<Streamable.Media.Sources?>,
val current: MutableStateFlow<Map<String, Streamable.Media.Sources>>,
extListFlow: MutableStateFlow<List<MusicExtension>?>,
cache: SimpleCache,
settings: SharedPreferences,
) : MediaSource.Factory {

private val dataSource = ResolvingDataSource.Factory(
CustomCacheDataSource.Factory(cache, StreamableDataSource.Factory(context)),
CacheDataSource.Factory()
.setCache(cache)
.setUpstreamDataSourceFactory(StreamableDataSource.Factory(context)),
StreamableResolver(current)
)

private val default = lazily { DefaultMediaSourceFactory(dataSource) }
private val hls = lazily { HlsMediaSource.Factory(dataSource) }
private val dash = lazily { DashMediaSource.Factory(dataSource) }

private val provider = DefaultDrmSessionManagerProvider().apply {
setDrmHttpDataSourceFactory(dataSource)
}
Expand Down Expand Up @@ -174,19 +184,13 @@ class StreamableMediaSource(
return this
}

fun create(mediaItem: MediaItem, index: Int, source: Streamable.Source): MediaSource {
val type = (source as? Streamable.Source.Http)?.type
val factory = when (type) {
Streamable.SourceType.DASH -> dash
Streamable.SourceType.HLS -> hls
Streamable.SourceType.Progressive, null -> default
}
val new = MediaItemUtils.buildForSource(mediaItem, index, source)
return factory.value.createMediaSource(new)
}
private val default = lazily { DefaultMediaSourceFactory(dataSource) }
private val hls = lazily { HlsMediaSource.Factory(dataSource) }
private val dash = lazily { DashMediaSource.Factory(dataSource) }
private val loader = StreamableLoader(context, settings, extListFlow)

val loader = StreamableLoader(context, settings, extListFlow)
override fun createMediaSource(mediaItem: MediaItem) =
StreamableMediaSource(mediaItem, this)
override fun createMediaSource(mediaItem: MediaItem) = StreamableMediaSource(
context, scope, current, loader, dash, hls, default, mediaItem
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,15 @@ import dev.brahmkshatriya.echo.playback.MediaItemUtils.toIdAndIndex
import kotlinx.coroutines.flow.MutableStateFlow

class StreamableResolver(
private val current: MutableStateFlow<Streamable.Media.Sources?>
private val current: MutableStateFlow<Map<String, Streamable.Media.Sources>>
) : Resolver {

@UnstableApi
override fun resolveDataSpec(dataSpec: DataSpec): DataSpec {
val (_, _, index) = dataSpec.uri.toString().toIdAndIndex() ?: return dataSpec

val current = current.value ?: return dataSpec
val source = current.sources[index]
val (id, _, index) = dataSpec.uri.toString().toIdAndIndex() ?: return dataSpec
val streamable = current.value[id]!!
return dataSpec.copy(
customData = source
customData = streamable.sources[index]
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class TrackDetailsFragment : Fragment() {
private val playerViewModel: PlayerViewModel
) : RecyclerView.Adapter<InfoAdapter.ViewHolder>() {

private var sources: Streamable.Media.Sources? = null
private var sources = mapOf<String, Streamable.Media.Sources>()
private var item: MediaItem? = null
private var tracks: Tracks? = null
private var player: Player? = null
Expand All @@ -140,9 +140,12 @@ class TrackDetailsFragment : Fragment() {

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
item?.let { binding.applyCurrent(it) }
item?.let {
binding.applyCurrent(it)
val source = sources[it.mediaId]
binding.applySources(source)
}
player?.let { binding.applyTracks(it, tracks ?: Tracks.EMPTY) }
sources?.let { binding.applySources(it) }
}

fun applyCurrent(item: MediaItem) {
Expand All @@ -156,7 +159,7 @@ class TrackDetailsFragment : Fragment() {
notifyDataSetChanged()
}

fun applySources(sources: Streamable.Media.Sources?) {
fun applySources(sources: Map<String, Streamable.Media.Sources>) {
this.sources = sources
notifyDataSetChanged()
}
Expand Down Expand Up @@ -336,7 +339,7 @@ class TrackDetailsFragment : Fragment() {
}

private fun ItemTrackInfoBinding.applySources(sources: Streamable.Media.Sources?) {
val list = if(sources != null && !sources.merged) sources.sources else listOf()
val list = if (sources != null && !sources.merged) sources.sources else listOf()
val context = root.context
applyChips(
list,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class PlayerViewModel @Inject constructor(
val app: Application,
val currentFlow: MutableStateFlow<Current?>,
val radioStateFlow: MutableStateFlow<Radio.State>,
val currentSources: MutableStateFlow<Streamable.Media.Sources?>,
val currentSources: MutableStateFlow<Map<String, Streamable.Media.Sources>>,
val cache: SimpleCache,
val fftAudioProcessor: FFTAudioProcessor,
private val mutableMessageFlow: MutableSharedFlow<SnackBar.Message>,
Expand Down Expand Up @@ -125,19 +125,19 @@ class PlayerViewModel @Inject constructor(
val extension = extensionListFlow.getExtension(clientId) ?: return@withContext null
when (lists) {
is EchoMediaItem.Lists.AlbumItem -> {
extension.get<AlbumClient, List<Track>>(throwableFlow){
extension.get<AlbumClient, List<Track>>(throwableFlow) {
loadTracks(lists.album).loadAll()
}
}

is EchoMediaItem.Lists.PlaylistItem -> {
extension.get<PlaylistClient, List<Track>>(throwableFlow){
extension.get<PlaylistClient, List<Track>>(throwableFlow) {
loadTracks(lists.playlist).loadAll()
}
}

is EchoMediaItem.Lists.RadioItem -> {
extension.get<RadioClient, List<Track>>(throwableFlow){
extension.get<RadioClient, List<Track>>(throwableFlow) {
loadTracks(lists.radio).loadAll()
}
}
Expand Down

0 comments on commit d87690c

Please sign in to comment.