Skip to content

Commit

Permalink
Refactor exoplayer media factory
Browse files Browse the repository at this point in the history
  • Loading branch information
brahmkshatriya committed Nov 9, 2024
1 parent 602da3d commit b2320c8
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 298 deletions.
6 changes: 3 additions & 3 deletions app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -448,7 +449,8 @@ class OfflineExtension(
playlist: Playlist, tracks: List<Track>, index: Int, new: List<Track>
) {
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)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package dev.brahmkshatriya.echo.playback.source
package dev.brahmkshatriya.echo.playback.loading

import android.net.Uri
import androidx.annotation.OptIn
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<DefaultDataSource.Factory>,
private val byteStreamDataSourceFactory: Lazy<ByteStreamDataSource.Factory>,
private val byteChannelDataSourceFactory: Lazy<ByteChannelDataSource.Factory>,
Expand All @@ -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
)
}
Expand Down
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
}
}
}
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)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package dev.brahmkshatriya.echo.playback.source
package dev.brahmkshatriya.echo.playback.loading

import android.net.Uri
import androidx.annotation.OptIn
Expand All @@ -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<Streamable.Media.Sources?>
) : Resolver {

Expand Down
Loading

0 comments on commit b2320c8

Please sign in to comment.