diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt index bb7c0ec3..da4a0f69 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt @@ -26,10 +26,10 @@ import dev.brahmkshatriya.echo.playback.listeners.AudioFocusListener import dev.brahmkshatriya.echo.playback.listeners.PlayerEventListener import dev.brahmkshatriya.echo.playback.listeners.Radio import dev.brahmkshatriya.echo.playback.listeners.TrackingListener +import dev.brahmkshatriya.echo.playback.loading.StreamableMediaSource import dev.brahmkshatriya.echo.playback.render.FFTAudioProcessor import dev.brahmkshatriya.echo.playback.render.PlayerBitmapLoader import dev.brahmkshatriya.echo.playback.render.RenderersFactory -import dev.brahmkshatriya.echo.playback.source.MediaFactory import dev.brahmkshatriya.echo.ui.settings.AudioFragment.AudioPreference.Companion.CLOSE_PLAYER import dev.brahmkshatriya.echo.ui.settings.AudioFragment.AudioPreference.Companion.SKIP_SILENCE import dev.brahmkshatriya.echo.viewmodels.SnackBar @@ -88,8 +88,8 @@ class PlayerService : MediaLibraryService() { .setIsSpeedChangeSupportRequired(true) .build() - val factory = MediaFactory( - cache, currentSources, this, scope, extListFlow, settings, throwFlow + val factory = StreamableMediaSource.Factory( + this, scope, currentSources, extListFlow, cache, settings ) ExoPlayer.Builder(this, factory) 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 510bcf9b..b085d1a0 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt @@ -419,7 +419,8 @@ class OfflineExtension( override suspend fun likeTrack(track: Track, isLiked: Boolean) { val playlist = library.likedPlaylist.id - if (isLiked) context.addSongToPlaylist(playlist, track.id.toLong(), 0) + val id = track.id.substringAfter("offline:").toLong() + if (isLiked) context.addSongToPlaylist(playlist, id, 0) else { val index = library.likedPlaylist.songList.indexOfFirst { it.id == track.id } context.removeSongFromPlaylist(playlist, index) @@ -448,7 +449,8 @@ class OfflineExtension( playlist: Playlist, tracks: List, index: Int, new: List ) { new.forEach { - context.addSongToPlaylist(playlist.id.toLong(), it.id.toLong(), index) + val id = it.id.substringAfter("offline:").toLong() + context.addSongToPlaylist(playlist.id.toLong(), id, index) } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/ByteChannelDataSource.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/ByteChannelDataSource.kt similarity index 97% rename from app/src/main/java/dev/brahmkshatriya/echo/playback/source/ByteChannelDataSource.kt rename to app/src/main/java/dev/brahmkshatriya/echo/playback/loading/ByteChannelDataSource.kt index 5b3981d9..a141d896 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/ByteChannelDataSource.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/ByteChannelDataSource.kt @@ -1,4 +1,4 @@ -package dev.brahmkshatriya.echo.playback.source +package dev.brahmkshatriya.echo.playback.loading import androidx.annotation.OptIn import androidx.core.net.toUri diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/ByteStreamDataSource.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/ByteStreamDataSource.kt similarity index 96% rename from app/src/main/java/dev/brahmkshatriya/echo/playback/source/ByteStreamDataSource.kt rename to app/src/main/java/dev/brahmkshatriya/echo/playback/loading/ByteStreamDataSource.kt index e1ff2a77..883ade9c 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/ByteStreamDataSource.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/ByteStreamDataSource.kt @@ -1,4 +1,4 @@ -package dev.brahmkshatriya.echo.playback.source +package dev.brahmkshatriya.echo.playback.loading import androidx.annotation.OptIn import androidx.core.net.toUri diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/CustomCacheDataSource.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/CustomCacheDataSource.kt similarity index 97% rename from app/src/main/java/dev/brahmkshatriya/echo/playback/source/CustomCacheDataSource.kt rename to app/src/main/java/dev/brahmkshatriya/echo/playback/loading/CustomCacheDataSource.kt index c114887d..65f6f1a0 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/CustomCacheDataSource.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/CustomCacheDataSource.kt @@ -1,4 +1,4 @@ -package dev.brahmkshatriya.echo.playback.source +package dev.brahmkshatriya.echo.playback.loading import android.net.Uri import androidx.annotation.OptIn diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/MediaDataSource.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableDataSource.kt similarity index 90% rename from app/src/main/java/dev/brahmkshatriya/echo/playback/source/MediaDataSource.kt rename to app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableDataSource.kt index f3769297..bce4436b 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/MediaDataSource.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableDataSource.kt @@ -1,4 +1,4 @@ -package dev.brahmkshatriya.echo.playback.source +package dev.brahmkshatriya.echo.playback.loading import android.content.Context import androidx.core.net.toUri @@ -8,10 +8,10 @@ import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSpec import androidx.media3.datasource.DefaultDataSource import dev.brahmkshatriya.echo.common.models.Streamable -import dev.brahmkshatriya.echo.playback.source.MediaResolver.Companion.copy +import dev.brahmkshatriya.echo.playback.loading.StreamableResolver.Companion.copy @UnstableApi -class MediaDataSource( +class StreamableDataSource( private val defaultDataSourceFactory: Lazy, private val byteStreamDataSourceFactory: Lazy, private val byteChannelDataSourceFactory: Lazy, @@ -23,7 +23,7 @@ class MediaDataSource( private val defaultDataSourceFactory = lazy { DefaultDataSource.Factory(context) } private val byteStreamDataSourceFactory = lazy { ByteStreamDataSource.Factory() } private val byteChannelDataSourceFactory = lazy { ByteChannelDataSource.Factory() } - override fun createDataSource() = MediaDataSource( + override fun createDataSource() = StreamableDataSource( defaultDataSourceFactory, byteStreamDataSourceFactory, byteChannelDataSourceFactory ) } 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 new file mode 100644 index 00000000..75fe8c5f --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableLoader.kt @@ -0,0 +1,92 @@ +package dev.brahmkshatriya.echo.playback.loading + +import android.content.Context +import android.content.SharedPreferences +import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi +import dev.brahmkshatriya.echo.R +import dev.brahmkshatriya.echo.common.MusicExtension +import dev.brahmkshatriya.echo.common.clients.TrackClient +import dev.brahmkshatriya.echo.common.models.Streamable +import dev.brahmkshatriya.echo.extensions.getExtension +import dev.brahmkshatriya.echo.playback.MediaItemUtils +import dev.brahmkshatriya.echo.playback.MediaItemUtils.backgroundIndex +import dev.brahmkshatriya.echo.playback.MediaItemUtils.clientId +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 +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext + +@UnstableApi +class StreamableLoader( + private val context: Context, + private val settings: SharedPreferences, + private val extensionListFlow: MutableStateFlow?>, +) { + suspend fun load(mediaItem: MediaItem) = withContext(Dispatchers.IO) { + extensionListFlow.first { it != null } + 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() + } + + private suspend fun withClient( + mediaItem: MediaItem, + block: suspend TrackClient.() -> T + ): T { + val extension = extensionListFlow.getExtension(mediaItem.clientId) + ?: throw Exception(context.noClient().message) + 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) } + } + + private suspend fun loadTrack(item: MediaItem) = withClient(item) { + loadTrack(item.track).also { + it.sources.ifEmpty { + throw Exception(context.getString(R.string.no_streams_found)) + } + } + } + + private suspend fun loadSources(mediaItem: MediaItem): Streamable.Media.Sources { + val streams = mediaItem.track.sources + val index = mediaItem.sourcesIndex + val streamable = streams[index] + return withClient(mediaItem) { + getStreamableMedia(streamable) as Streamable.Media.Sources + } + } + + private suspend fun loadBackground(mediaItem: MediaItem): Streamable.Media.Background { + val streams = mediaItem.track.backgrounds + val index = mediaItem.backgroundIndex + val streamable = streams[index] + return withClient(mediaItem) { + getStreamableMedia(streamable) as Streamable.Media.Background + } + } + + private suspend fun loadSubtitle(mediaItem: MediaItem): Streamable.Media.Subtitle { + val streams = mediaItem.track.subtitles + val index = mediaItem.subtitleIndex + val streamable = streams[index] + return withClient(mediaItem) { + getStreamableMedia(streamable) as Streamable.Media.Subtitle + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..e37dad2b --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableMediaSource.kt @@ -0,0 +1,192 @@ +package dev.brahmkshatriya.echo.playback.loading + +import android.content.Context +import android.content.SharedPreferences +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.Timeline +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.SimpleCache +import androidx.media3.exoplayer.dash.DashMediaSource +import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider +import androidx.media3.exoplayer.drm.DrmSessionManagerProvider +import androidx.media3.exoplayer.hls.HlsMediaSource +import androidx.media3.exoplayer.source.CompositeMediaSource +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.exoplayer.source.MediaPeriod +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.MergingMediaSource +import androidx.media3.exoplayer.upstream.Allocator +import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy +import dev.brahmkshatriya.echo.R +import dev.brahmkshatriya.echo.common.MusicExtension +import dev.brahmkshatriya.echo.common.models.Streamable +import dev.brahmkshatriya.echo.offline.OfflineExtension +import dev.brahmkshatriya.echo.playback.MediaItemUtils +import dev.brahmkshatriya.echo.playback.MediaItemUtils.backgroundIndex +import dev.brahmkshatriya.echo.playback.MediaItemUtils.clientId +import dev.brahmkshatriya.echo.playback.MediaItemUtils.sourceIndex +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.utils.saveToCache +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +@UnstableApi +class StreamableMediaSource( + private var mediaItem: MediaItem, + private val factory: Factory, +) : CompositeMediaSource() { + + 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 + + override fun prepareSourceInternal(mediaTransferListener: TransferListener?) { + super.prepareSourceInternal(mediaTransferListener) + val handler = Util.createHandlerForCurrentLooper() + scope.launch { + val (item, sources) = runCatching { loader.load(mediaItem) }.getOrElse { + error = it + return@launch + } + onResolved(item, sources) + handler.post { prepareChildSource(null, actualSource) } + } + } + + 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) + } + } + } + } + + override fun onChildSourceInfoRefreshed( + childSourceId: Nothing?, mediaSource: MediaSource, newTimeline: Timeline + ) = refreshSourceInfo(newTimeline) + + override fun getMediaItem() = mediaItem + + override fun createPeriod( + id: MediaSource.MediaPeriodId, allocator: Allocator, startPositionUs: Long + ) = actualSource.createPeriod(id, allocator, startPositionUs) + + override fun releasePeriod(mediaPeriod: MediaPeriod) = + actualSource.releasePeriod(mediaPeriod) + + override fun canUpdateMediaItem(mediaItem: MediaItem) = run { + this.mediaItem.apply { + if (sourcesIndex != mediaItem.sourcesIndex) return@run false + if (sourceIndex != mediaItem.sourceIndex) return@run false + if (backgroundIndex != mediaItem.backgroundIndex) return@run false + if (subtitleIndex != mediaItem.subtitleIndex) return@run false + } + actualSource.canUpdateMediaItem(mediaItem) + } + + override fun updateMediaItem(mediaItem: MediaItem) { + this.mediaItem = mediaItem + actualSource.updateMediaItem(mediaItem) + } + + + @UnstableApi + class Factory( + val context: Context, + val scope: CoroutineScope, + val current: MutableStateFlow, + extListFlow: MutableStateFlow?>, + cache: SimpleCache, + settings: SharedPreferences, + ) : MediaSource.Factory { + + private val dataSource = ResolvingDataSource.Factory( + CustomCacheDataSource.Factory(cache, 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) + } + + private var drmSessionManagerProvider: DrmSessionManagerProvider? = provider + private var loadErrorHandlingPolicy: LoadErrorHandlingPolicy? = null + private fun lazily(factory: () -> MediaSource.Factory) = lazy { + factory().apply { + drmSessionManagerProvider?.let { setDrmSessionManagerProvider(it) } + loadErrorHandlingPolicy?.let { setLoadErrorHandlingPolicy(it) } + } + } + + override fun getSupportedTypes() = intArrayOf( + C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_HLS, C.CONTENT_TYPE_DASH + ) + + override fun setDrmSessionManagerProvider( + drmSessionManagerProvider: DrmSessionManagerProvider + ): MediaSource.Factory { + this.drmSessionManagerProvider = drmSessionManagerProvider + return this + } + + override fun setLoadErrorHandlingPolicy( + loadErrorHandlingPolicy: LoadErrorHandlingPolicy + ): MediaSource.Factory { + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy + 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) + } + + val loader = StreamableLoader(context, settings, extListFlow) + override fun createMediaSource(mediaItem: MediaItem) = + StreamableMediaSource(mediaItem, this) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/MediaResolver.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableResolver.kt similarity index 96% rename from app/src/main/java/dev/brahmkshatriya/echo/playback/source/MediaResolver.kt rename to app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableResolver.kt index d211daae..2cee6b4d 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/MediaResolver.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableResolver.kt @@ -1,4 +1,4 @@ -package dev.brahmkshatriya.echo.playback.source +package dev.brahmkshatriya.echo.playback.loading import android.net.Uri import androidx.annotation.OptIn @@ -9,7 +9,7 @@ import dev.brahmkshatriya.echo.common.models.Streamable import dev.brahmkshatriya.echo.playback.MediaItemUtils.toIdAndIndex import kotlinx.coroutines.flow.MutableStateFlow -class MediaResolver( +class StreamableResolver( private val current: MutableStateFlow ) : Resolver { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/DelayedSource.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/source/DelayedSource.kt deleted file mode 100644 index 8f14a914..00000000 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/DelayedSource.kt +++ /dev/null @@ -1,197 +0,0 @@ -package dev.brahmkshatriya.echo.playback.source - -import android.content.Context -import androidx.annotation.OptIn -import androidx.media3.common.MediaItem -import androidx.media3.common.Timeline -import androidx.media3.common.util.UnstableApi -import androidx.media3.datasource.TransferListener -import androidx.media3.exoplayer.source.CompositeMediaSource -import androidx.media3.exoplayer.source.MediaPeriod -import androidx.media3.exoplayer.source.MediaSource -import androidx.media3.exoplayer.source.MergingMediaSource -import androidx.media3.exoplayer.upstream.Allocator -import dev.brahmkshatriya.echo.R -import dev.brahmkshatriya.echo.common.MusicExtension -import dev.brahmkshatriya.echo.common.clients.TrackClient -import dev.brahmkshatriya.echo.common.models.Streamable -import dev.brahmkshatriya.echo.extensions.getExtension -import dev.brahmkshatriya.echo.offline.OfflineExtension -import dev.brahmkshatriya.echo.playback.MediaItemUtils -import dev.brahmkshatriya.echo.playback.MediaItemUtils.backgroundIndex -import dev.brahmkshatriya.echo.playback.MediaItemUtils.clientId -import dev.brahmkshatriya.echo.playback.MediaItemUtils.isLoaded -import dev.brahmkshatriya.echo.playback.MediaItemUtils.sourceIndex -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.utils.saveToCache -import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel.Companion.noClient -import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel.Companion.trackNotSupported -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import java.io.IOException - -@OptIn(UnstableApi::class) -class DelayedSource( - private var mediaItem: MediaItem, - private val mediaFactory: MediaFactory, -) : CompositeMediaSource() { - - private val scope = mediaFactory.scope - private val context = mediaFactory.context - private val currentSources = mediaFactory.current - private val extensionListFlow = mediaFactory.extListFlow - private val settings = mediaFactory.settings - private val throwableFlow = mediaFactory.throwableFlow - - private lateinit var actualSource: MediaSource - override fun prepareSourceInternal(mediaTransferListener: TransferListener?) { - super.prepareSourceInternal(mediaTransferListener) - scope.launch(Dispatchers.IO) { - val (new, streamable) = runCatching { resolve(mediaItem) }.getOrElse { - throwableFlow.emit(it) - return@launch - } - currentSources.value = streamable - onUrlResolved(new, streamable) - } - } - - private suspend fun onUrlResolved( - new: MediaItem, streamable: Streamable.Media.Sources, - ) = withContext(Dispatchers.Main) { - mediaItem = new - - 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 -> mediaFactory.create(new, 0, sources.first()) - else -> { - if(streamable.merged) MergingMediaSource( - *sources.mapIndexed { index, source -> - mediaFactory.create(new, index, source) - }.toTypedArray() - ) else { - val index = mediaItem.sourceIndex - val source = sources[index] - mediaFactory.create(new, index, source) - } - } - } - runCatching { prepareChildSource(null, actualSource) } - } - - override fun maybeThrowSourceInfoRefreshError() { - runCatching { - super.maybeThrowSourceInfoRefreshError() - }.getOrElse { - if (it is IOException) throw it - else runBlocking { - if (it is NullPointerException) return@runBlocking - throwableFlow.emit(it) - } - } - } - - override fun onChildSourceInfoRefreshed( - childSourceId: Nothing?, mediaSource: MediaSource, newTimeline: Timeline - ) = refreshSourceInfo(newTimeline) - - override fun getMediaItem() = mediaItem - - override fun createPeriod( - id: MediaSource.MediaPeriodId, allocator: Allocator, startPositionUs: Long - ) = actualSource.createPeriod(id, allocator, startPositionUs) - - override fun releasePeriod(mediaPeriod: MediaPeriod) = - actualSource.releasePeriod(mediaPeriod) - - override fun canUpdateMediaItem(mediaItem: MediaItem) = run { - this.mediaItem.apply { - if (sourcesIndex != mediaItem.sourcesIndex) return@run false - if (sourceIndex != mediaItem.sourceIndex) return@run false - if (backgroundIndex != mediaItem.backgroundIndex) return@run false - if (subtitleIndex != mediaItem.subtitleIndex) return@run false - } - actualSource.canUpdateMediaItem(mediaItem) - } - - override fun updateMediaItem(mediaItem: MediaItem) { - this.mediaItem = mediaItem - actualSource.updateMediaItem(mediaItem) - } - - private suspend fun resolve(mediaItem: MediaItem) = coroutineScope { - extensionListFlow.first { it != null } - val new = if (mediaItem.isLoaded) mediaItem - else MediaItemUtils.buildLoaded(settings, mediaItem, loadTrack(mediaItem)) - val sources = async { loadSources(new) } - val background = async { if (new.backgroundIndex < 0) null else loadVideo(new) } - val subtitle = async { if (new.subtitleIndex < 0) null else loadSubtitle(new) } - MediaItemUtils.buildExternal(new, background.await(), subtitle.await()) to sources.await() - } - - private suspend fun loadTrack(item: MediaItem) = - item.getTrackClient(context, extensionListFlow) { - loadTrack(item.track).also { - it.sources.ifEmpty { - throw Exception(context.getString(R.string.no_streams_found)) - } - } - } - - private suspend fun loadSources(mediaItem: MediaItem): Streamable.Media.Sources { - val streams = mediaItem.track.sources - val index = mediaItem.sourcesIndex - val streamable = streams[index] - return mediaItem.getTrackClient(context, extensionListFlow) { - getStreamableMedia(streamable) as Streamable.Media.Sources - } - } - - private suspend fun loadVideo(mediaItem: MediaItem): Streamable.Media.Background { - val streams = mediaItem.track.backgrounds - val index = mediaItem.backgroundIndex - val streamable = streams[index] - return mediaItem.getTrackClient(context, extensionListFlow) { - getStreamableMedia(streamable) as Streamable.Media.Background - } - } - - private suspend fun loadSubtitle(mediaItem: MediaItem): Streamable.Media.Subtitle { - val streams = mediaItem.track.subtitles - val index = mediaItem.subtitleIndex - val streamable = streams[index] - return mediaItem.getTrackClient(context, extensionListFlow) { - getStreamableMedia(streamable) as Streamable.Media.Subtitle - } - } - - companion object { - suspend fun MediaItem.getTrackClient( - context: Context, - extensionListFlow: MutableStateFlow?>, - block: suspend TrackClient.() -> T - ): T { - val extension = extensionListFlow.getExtension(clientId) - ?: throw Exception(context.noClient().message) - 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) } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/MediaFactory.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/source/MediaFactory.kt deleted file mode 100644 index 8a9722c9..00000000 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/source/MediaFactory.kt +++ /dev/null @@ -1,87 +0,0 @@ -package dev.brahmkshatriya.echo.playback.source - -import android.content.Context -import android.content.SharedPreferences -import androidx.media3.common.C -import androidx.media3.common.MediaItem -import androidx.media3.common.util.UnstableApi -import androidx.media3.datasource.ResolvingDataSource -import androidx.media3.datasource.cache.SimpleCache -import androidx.media3.exoplayer.dash.DashMediaSource -import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider -import androidx.media3.exoplayer.drm.DrmSessionManagerProvider -import androidx.media3.exoplayer.hls.HlsMediaSource -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory -import androidx.media3.exoplayer.source.MediaSource -import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy -import dev.brahmkshatriya.echo.common.MusicExtension -import dev.brahmkshatriya.echo.common.models.Streamable -import dev.brahmkshatriya.echo.playback.MediaItemUtils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow - -@UnstableApi -class MediaFactory( - cache: SimpleCache, - val current : MutableStateFlow, - val context: Context, - val scope: CoroutineScope, - val extListFlow: MutableStateFlow?>, - val settings: SharedPreferences, - val throwableFlow: MutableSharedFlow -) : MediaSource.Factory { - - private val mediaResolver = MediaResolver(current) - private val dataSource = ResolvingDataSource.Factory( - CustomCacheDataSource.Factory(cache, MediaDataSource.Factory(context)), - mediaResolver - ) - 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) - } - - private var drmSessionManagerProvider: DrmSessionManagerProvider? = provider - private var loadErrorHandlingPolicy: LoadErrorHandlingPolicy? = null - private fun lazily(factory: () -> MediaSource.Factory) = lazy { - factory().apply { - drmSessionManagerProvider?.let { setDrmSessionManagerProvider(it) } - loadErrorHandlingPolicy?.let { setLoadErrorHandlingPolicy(it) } - } - } - - override fun getSupportedTypes() = intArrayOf( - C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_HLS, C.CONTENT_TYPE_DASH - ) - - override fun setDrmSessionManagerProvider( - drmSessionManagerProvider: DrmSessionManagerProvider - ): MediaSource.Factory { - this.drmSessionManagerProvider = drmSessionManagerProvider - return this - } - - override fun setLoadErrorHandlingPolicy( - loadErrorHandlingPolicy: LoadErrorHandlingPolicy - ): MediaSource.Factory { - this.loadErrorHandlingPolicy = loadErrorHandlingPolicy - return this - } - - override fun createMediaSource(mediaItem: MediaItem) = DelayedSource(mediaItem, 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) - } -} \ No newline at end of file