From d87690c5726690028b7e8af7f048b09b121245dc Mon Sep 17 00:00:00 2001 From: brahmkshatriya <69040506+brahmkshatriya@users.noreply.github.com> Date: Mon, 11 Nov 2024 00:21:50 +0530 Subject: [PATCH] Fix replaying of previous song --- .../dev/brahmkshatriya/echo/PlayerService.kt | 2 +- .../dev/brahmkshatriya/echo/di/AppModule.kt | 2 +- .../echo/offline/MediaStoreUtils.kt | 2 +- .../echo/offline/OfflineExtension.kt | 6 +- .../echo/offline/TestExtension.kt | 7 +- .../echo/playback/MediaItemUtils.kt | 2 +- .../echo/playback/loading/StreamableLoader.kt | 9 +- .../playback/loading/StreamableMediaSource.kt | 114 +++++++++--------- .../playback/loading/StreamableResolver.kt | 10 +- .../echo/ui/player/TrackDetailsFragment.kt | 13 +- .../echo/viewmodels/PlayerViewModel.kt | 8 +- 11 files changed, 92 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt index da4a0f6..f373406 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt @@ -67,7 +67,7 @@ class PlayerService : MediaLibraryService() { lateinit var current: MutableStateFlow @Inject - lateinit var currentSources: MutableStateFlow + lateinit var currentSources: MutableStateFlow> @Inject lateinit var fftAudioProcessor: FFTAudioProcessor diff --git a/app/src/main/java/dev/brahmkshatriya/echo/di/AppModule.kt b/app/src/main/java/dev/brahmkshatriya/echo/di/AppModule.kt index dab34aa..5249738 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/di/AppModule.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/di/AppModule.kt @@ -68,7 +68,7 @@ class AppModule { @Provides @Singleton - fun provideCurrentSourcesFlow() = MutableStateFlow(null) + fun provideCurrentSourcesFlow() = MutableStateFlow(mapOf()) @Provides @Singleton diff --git a/app/src/main/java/dev/brahmkshatriya/echo/offline/MediaStoreUtils.kt b/app/src/main/java/dev/brahmkshatriya/echo/offline/MediaStoreUtils.kt index 381399f..fed392c 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/offline/MediaStoreUtils.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/offline/MediaStoreUtils.kt @@ -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, diff --git a/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt b/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt index b085d1a..510bcf9 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt @@ -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) @@ -449,8 +448,7 @@ class OfflineExtension( playlist: Playlist, tracks: List, index: Int, new: List ) { 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) } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt b/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt index 017b58b..eef53a6 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt @@ -135,7 +135,12 @@ class TestExtension : ExtensionClient, LoginClient.UsernamePassword, TrackClient } private val radio = Radio("empty", "empty") - override fun loadTracks(radio: Radio) = PagedData.Single { 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 diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/MediaItemUtils.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/MediaItemUtils.kt index c0bc87a..a710325 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/MediaItemUtils.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/MediaItemUtils.kt @@ -107,7 +107,7 @@ object MediaItemUtils { item.build() } - fun buildExternal( + fun buildWithBackgroundAndSubtitle( mediaItem: MediaItem, background: Streamable.Media.Background?, subtitle: Streamable.Media.Subtitle? diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableLoader.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableLoader.kt index 75fe8c5..6f6f416 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableLoader.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableLoader.kt @@ -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 @@ -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 withClient( @@ -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) { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableMediaSource.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableMediaSource.kt index e37dad2..07045d9 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableMediaSource.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableMediaSource.kt @@ -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 @@ -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>, + private val loader: StreamableLoader, + private val dash: Lazy, + private val hls: Lazy, + private val default: Lazy, + private var mediaItem: MediaItem ) : CompositeMediaSource() { + 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) } + } } } @@ -123,26 +136,23 @@ class StreamableMediaSource( actualSource.updateMediaItem(mediaItem) } - @UnstableApi class Factory( val context: Context, val scope: CoroutineScope, - val current: MutableStateFlow, + val current: MutableStateFlow>, extListFlow: MutableStateFlow?>, 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) } @@ -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 + ) } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableResolver.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableResolver.kt index 2cee6b4..750a26f 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableResolver.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableResolver.kt @@ -10,17 +10,15 @@ import dev.brahmkshatriya.echo.playback.MediaItemUtils.toIdAndIndex import kotlinx.coroutines.flow.MutableStateFlow class StreamableResolver( - private val current: MutableStateFlow + private val current: MutableStateFlow> ) : 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] ) } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/TrackDetailsFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/TrackDetailsFragment.kt index 6aa2332..62055be 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/TrackDetailsFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/TrackDetailsFragment.kt @@ -122,7 +122,7 @@ class TrackDetailsFragment : Fragment() { private val playerViewModel: PlayerViewModel ) : RecyclerView.Adapter() { - private var sources: Streamable.Media.Sources? = null + private var sources = mapOf() private var item: MediaItem? = null private var tracks: Tracks? = null private var player: Player? = null @@ -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) { @@ -156,7 +159,7 @@ class TrackDetailsFragment : Fragment() { notifyDataSetChanged() } - fun applySources(sources: Streamable.Media.Sources?) { + fun applySources(sources: Map) { this.sources = sources notifyDataSetChanged() } @@ -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, diff --git a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/PlayerViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/PlayerViewModel.kt index 1f38d2c..9eefecd 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/PlayerViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/PlayerViewModel.kt @@ -55,7 +55,7 @@ class PlayerViewModel @Inject constructor( val app: Application, val currentFlow: MutableStateFlow, val radioStateFlow: MutableStateFlow, - val currentSources: MutableStateFlow, + val currentSources: MutableStateFlow>, val cache: SimpleCache, val fftAudioProcessor: FFTAudioProcessor, private val mutableMessageFlow: MutableSharedFlow, @@ -125,19 +125,19 @@ class PlayerViewModel @Inject constructor( val extension = extensionListFlow.getExtension(clientId) ?: return@withContext null when (lists) { is EchoMediaItem.Lists.AlbumItem -> { - extension.get>(throwableFlow){ + extension.get>(throwableFlow) { loadTracks(lists.album).loadAll() } } is EchoMediaItem.Lists.PlaylistItem -> { - extension.get>(throwableFlow){ + extension.get>(throwableFlow) { loadTracks(lists.playlist).loadAll() } } is EchoMediaItem.Lists.RadioItem -> { - extension.get>(throwableFlow){ + extension.get>(throwableFlow) { loadTracks(lists.radio).loadAll() } }