-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
602da3d
commit b2320c8
Showing
11 changed files
with
300 additions
and
298 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
.../playback/source/ByteChannelDataSource.kt → ...playback/loading/ByteChannelDataSource.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...o/playback/source/ByteStreamDataSource.kt → .../playback/loading/ByteStreamDataSource.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
.../playback/source/CustomCacheDataSource.kt → ...playback/loading/CustomCacheDataSource.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableLoader.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<List<MusicExtension>?>, | ||
) { | ||
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 <T> 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 | ||
} | ||
} | ||
} |
192 changes: 192 additions & 0 deletions
192
app/src/main/java/dev/brahmkshatriya/echo/playback/loading/StreamableMediaSource.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Nothing>() { | ||
|
||
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<Streamable.Media.Sources?>, | ||
extListFlow: MutableStateFlow<List<MusicExtension>?>, | ||
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.