Skip to content

Commit

Permalink
Add subtitle support and auto refresh extensions upon app installation
Browse files Browse the repository at this point in the history
  • Loading branch information
brahmkshatriya committed Sep 2, 2024
1 parent 7adefd9 commit 7727467
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 64 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ dependencies {
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")

implementation("androidx.fragment:fragment-ktx:1.8.2")
implementation("androidx.fragment:fragment-ktx:1.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4")
implementation("androidx.paging:paging-common-ktx:3.3.2")
implementation("androidx.paging:paging-runtime-ktx:3.3.2")
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/dev/brahmkshatriya/echo/PlaybackService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ class PlaybackService : MediaLibraryService() {
}
}

//TODO: Quality Selection
//TODO: Open .eapk files
//TODO: extension updater
//TODO: Spotify
//TODO: EQ, Pitch, Tempo, Reverb & Sleep Timer(5m, 10m, 15m, 30m, 45m, 1hr, End of track)
// val equalizer = Equalizer(1, exoPlayer.audioSessionId)

this.mediaLibrarySession = session
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dev.brahmkshatriya.echo.offline.LocalExtensionRepo
import dev.brahmkshatriya.echo.plugger.ApkPluginSource
import dev.brahmkshatriya.echo.plugger.ExtensionMetadata
import dev.brahmkshatriya.echo.plugger.FileSystemPluginSource
import dev.brahmkshatriya.echo.plugger.ImportType
import dev.brahmkshatriya.echo.offline.LocalExtensionRepo
import dev.brahmkshatriya.echo.plugger.LyricsExtension
import dev.brahmkshatriya.echo.plugger.LyricsExtensionRepo
import dev.brahmkshatriya.echo.plugger.MusicExtension
Expand All @@ -24,7 +25,7 @@ import tel.jeelpa.plugger.PluginRepo
import tel.jeelpa.plugger.PluginRepoImpl
import tel.jeelpa.plugger.RepoComposer
import tel.jeelpa.plugger.pluginloader.AndroidPluginLoader
import tel.jeelpa.plugger.pluginloader.apk.ApkPluginSource
import java.io.File
import javax.inject.Singleton

@Module
Expand All @@ -36,14 +37,15 @@ class ExtensionModule {
@Singleton
fun providesRefresher(): MutableSharedFlow<Boolean> = MutableStateFlow(false)

private fun Context.getPluginFileDir() = File(filesDir, "extensions").apply { mkdirs() }
private fun <T> getComposed(
context: Context,
suffix: String,
vararg repo: PluginRepo<ExtensionMetadata, T>
): RepoComposer<ExtensionMetadata, T> {
val loader = AndroidPluginLoader<ExtensionMetadata, T>(context)
val apkFilePluginRepo = PluginRepoImpl(
FileSystemPluginSource(context.filesDir, ".apk"),
FileSystemPluginSource(context.getPluginFileDir(), ".eapk"),
ApkFileManifestParser(context.packageManager, ApkManifestParser(ImportType.Apk)),
loader,
)
Expand Down
27 changes: 21 additions & 6 deletions app/src/main/java/dev/brahmkshatriya/echo/offline/TestExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import dev.brahmkshatriya.echo.common.models.Streamable
import dev.brahmkshatriya.echo.common.models.Streamable.Audio.Companion.toAudio
import dev.brahmkshatriya.echo.common.models.Streamable.Media.Companion.toAudioVideoMedia
import dev.brahmkshatriya.echo.common.models.Streamable.Media.Companion.toMedia
import dev.brahmkshatriya.echo.common.models.Streamable.Media.Companion.toSubtitleMedia
import dev.brahmkshatriya.echo.common.models.Streamable.Media.Companion.toVideoMedia
import dev.brahmkshatriya.echo.common.models.Tab
import dev.brahmkshatriya.echo.common.models.Track
Expand Down Expand Up @@ -60,6 +61,7 @@ class TestExtension : ExtensionClient, LoginClient.UsernamePassword, TrackClient
Streamable.MediaType.Audio -> streamable.id.toAudio().toMedia()
Streamable.MediaType.Video -> streamable.id.toVideoMedia()
Streamable.MediaType.AudioVideo -> streamable.id.toAudioVideoMedia()
Streamable.MediaType.Subtitle -> streamable.id.toSubtitleMedia(Streamable.SubtitleType.VTT)
}
}

Expand All @@ -75,6 +77,9 @@ class TestExtension : ExtensionClient, LoginClient.UsernamePassword, TrackClient
private val video =
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"

private val subtitle =
"https://gist.githubusercontent.com/samdutton/ca37f3adaf4e23679957b8083e061177/raw/e19399fbccbc069a2af4266e5120ae6bad62699a/sample.vtt"

private fun createTrack(id: String, title: String, streamables: List<Streamable>) = Track(
id,
title,
Expand All @@ -89,10 +94,16 @@ class TestExtension : ExtensionClient, LoginClient.UsernamePassword, TrackClient
createTrack("audio", "Audio", listOf(Streamable.audio(audio, 0))),
createTrack("video", "Video", listOf(Streamable.video(video, 0))),
createTrack(
"both", "Both", listOf(Streamable.audio(audio, 0), Streamable.video(video, 0))
"both", "Both", listOf(
Streamable.audio(audio, 0),
Streamable.video(video, 0),
Streamable.subtitle(subtitle)
)
),
createTrack(
"audioVideo", "Audio Video", listOf(Streamable.audioVideo(audio, 0))
"audioVideo",
"Audio Video",
listOf(Streamable.audioVideo(audio, 0), Streamable.subtitle(subtitle))
)
)
}
Expand Down Expand Up @@ -129,27 +140,31 @@ class TestExtension : ExtensionClient, LoginClient.UsernamePassword, TrackClient
playlist: Playlist,
title: String,
description: String?
) {}
) {
}

override suspend fun addTracksToPlaylist(
playlist: Playlist,
tracks: List<Track>,
index: Int,
new: List<Track>
) {}
) {
}

override suspend fun removeTracksFromPlaylist(
playlist: Playlist,
tracks: List<Track>,
indexes: List<Int>
) {}
) {
}

override suspend fun moveTrackInPlaylist(
playlist: Playlist,
tracks: List<Track>,
fromIndex: Int,
toIndex: Int
) {}
) {
}

override suspend fun loadPlaylist(playlist: Playlist) = playlist
override fun loadTracks(playlist: Playlist): PagedData<Track> = PagedData.Single {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import dev.brahmkshatriya.echo.common.models.Track
import dev.brahmkshatriya.echo.playback.MediaItemUtils.audioStreamable
import dev.brahmkshatriya.echo.playback.MediaItemUtils.clientId
import dev.brahmkshatriya.echo.playback.MediaItemUtils.isLoaded
import dev.brahmkshatriya.echo.playback.MediaItemUtils.subtitleIndex
import dev.brahmkshatriya.echo.playback.MediaItemUtils.track
import dev.brahmkshatriya.echo.playback.MediaItemUtils.video
import dev.brahmkshatriya.echo.playback.MediaItemUtils.videoIndex
Expand Down Expand Up @@ -80,7 +81,10 @@ class DelayedSource(
true -> videoFactory.create(new)
null -> FilteringMediaSource(audioFactory.create(new), C.TRACK_TYPE_AUDIO)
false -> MergingMediaSource(
FilteringMediaSource(videoFactory.create(new), C.TRACK_TYPE_VIDEO),
FilteringMediaSource(
videoFactory.create(new),
setOf(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_TEXT)
),
FilteringMediaSource(audioFactory.create(new), C.TRACK_TYPE_AUDIO)
)
}
Expand Down Expand Up @@ -109,9 +113,14 @@ class DelayedSource(
private suspend fun resolve(mediaItem: MediaItem): MediaItem {
val track = mediaItem.track
val loadedTrack = if (!mediaItem.isLoaded) loadTrack(mediaItem) else track
val newMediaItem = MediaItemUtils.build(settings, mediaItem, loadedTrack)
var newMediaItem = MediaItemUtils.build(settings, mediaItem, loadedTrack)
val video = if (mediaItem.videoIndex < 0) return newMediaItem else loadVideo(mediaItem)
return MediaItemUtils.build(newMediaItem, video)
newMediaItem = MediaItemUtils.build(newMediaItem, video)
println("subtitleIndex: ${mediaItem.subtitleIndex}")
val subtitle =
if (mediaItem.subtitleIndex < 0) return newMediaItem else loadSubtitle(mediaItem)
println("subtitle: $subtitle")
return MediaItemUtils.build(newMediaItem, subtitle)
}

private suspend fun loadTrack(
Expand Down Expand Up @@ -140,6 +149,15 @@ class DelayedSource(
}
}

private suspend fun loadSubtitle(mediaItem: MediaItem): Streamable.Media.Subtitle {
val streams = mediaItem.track.subtitleStreamables
val index = mediaItem.subtitleIndex
val streamable = streams[index]
return mediaItem.getTrackClient(context, extensionListFlow) {
getStreamableMedia(streamable) as Streamable.Media.Subtitle
}
}

private fun getTrackFromCache(id: String): Track? {
val track = context.getFromCache<Track>(id) ?: return null
return if (!track.isExpired()) track else null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import android.content.SharedPreferences
import android.os.Bundle
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.common.ThumbRating
import dev.brahmkshatriya.echo.common.models.EchoMediaItem
import dev.brahmkshatriya.echo.common.models.Streamable
Expand Down Expand Up @@ -35,42 +37,58 @@ object MediaItemUtils {
settings: SharedPreferences?,
mediaItem: MediaItem,
track: Track
): MediaItem =
with(mediaItem) {
val item = buildUpon()
val metadata =
track.toMetaData(mediaMetadata.extras!!, clientId, context, true, settings)
item.setMediaMetadata(metadata)
return item.build()
}
): MediaItem = with(mediaItem) {
val item = buildUpon()
val metadata =
track.toMetaData(mediaMetadata.extras!!, clientId, context, true, settings)
item.setMediaMetadata(metadata)
return item.build()
}

fun buildAudio(mediaItem: MediaItem, index: Int): MediaItem = with(mediaItem) {
val item = buildUpon()
val metadata = track.toMetaData(mediaMetadata.extras!!, audioStreamIndex = index)
item.setMediaMetadata(metadata)
return item.build()
}

fun buildVideo(mediaItem: MediaItem, index: Int): MediaItem = with(mediaItem) {
val item = buildUpon()
val metadata = track.toMetaData(mediaMetadata.extras!!, videoStreamIndex = index)
item.setMediaMetadata(metadata)
return item.build()
}

fun buildAudio(mediaItem: MediaItem, index: Int): MediaItem =
with(mediaItem) {
val item = buildUpon()
val metadata = track.toMetaData(mediaMetadata.extras!!, audioStreamIndex = index)
item.setMediaMetadata(metadata)
return item.build()
}
fun buildSubtitle(mediaItem: MediaItem, index: Int): MediaItem = with(mediaItem) {
val item = buildUpon()
val metadata = track.toMetaData(mediaMetadata.extras!!, subtitleIndex = index)
item.setMediaMetadata(metadata)
return item.build()
}

fun buildVideo(mediaItem: MediaItem, index: Int): MediaItem =
with(mediaItem) {
val item = buildUpon()
val metadata = track.toMetaData(mediaMetadata.extras!!, videoStreamIndex = index)
item.setMediaMetadata(metadata)
return item.build()
}
fun build(mediaItem: MediaItem, video: Streamable.Media.WithVideo) = with(mediaItem) {
val item = buildUpon()
val bundle = mediaMetadata.extras!!
bundle.putSerialized("video", video)
val metadata = mediaMetadata.buildUpon()
.setExtras(bundle)
.build()
item.setMediaMetadata(metadata)
item.build()
}

fun build(mediaItem: MediaItem, video: Streamable.Media.WithVideo): MediaItem =
with(mediaItem) {
val item = buildUpon()
val bundle = mediaMetadata.extras!!
bundle.putSerialized("video", video)
val metadata = mediaMetadata.buildUpon()
.setExtras(bundle)
.build()
item.setMediaMetadata(metadata)
return item.build()
}
fun build(mediaItem: MediaItem, subtitle: Streamable.Media.Subtitle) = with(mediaItem) {
val item = buildUpon()
item.setSubtitleConfigurations(
listOf(
MediaItem.SubtitleConfiguration.Builder(subtitle.url.toUri())
.setMimeType(subtitle.type.toMimeType())
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.build()
)
)
item.build()
}

private fun Track.toMetaData(
bundle: Bundle,
Expand All @@ -81,6 +99,7 @@ object MediaItemUtils {
video: Streamable.Media.WithVideo? = null,
audioStreamIndex: Int? = null,
videoStreamIndex: Int? = null,
subtitleIndex: Int? = null
) = MediaMetadata.Builder()
.setTitle(title)
.setArtist(artists.joinToString(", ") { it.name })
Expand All @@ -102,6 +121,10 @@ object MediaItemUtils {
"videoStream",
videoStreamIndex ?: selectVideoIndex(settings, videoStreamables)
)
putInt(
"subtitle",
subtitleIndex ?: 0.takeIf { subtitleStreamables.isNotEmpty() } ?: -1
)
}
)

Expand All @@ -114,6 +137,7 @@ object MediaItemUtils {
val MediaMetadata.context get() = extras?.getSerialized<EchoMediaItem?>("context")
val MediaMetadata.audioIndex get() = extras?.getInt("audioStream") ?: -1
val MediaMetadata.videoIndex get() = extras?.getInt("videoStream") ?: -1
val MediaMetadata.subtitleIndex get() = extras?.getInt("subtitle") ?: -1
val MediaMetadata.isLiked get() = (userRating as? ThumbRating)?.isThumbsUp == true
val MediaMetadata.video get() = extras?.getSerialized<Streamable.Media.WithVideo?>("video")

Expand All @@ -123,9 +147,16 @@ object MediaItemUtils {
val MediaItem.isLoaded get() = mediaMetadata.isLoaded
val MediaItem.audioIndex get() = mediaMetadata.audioIndex
val MediaItem.videoIndex get() = mediaMetadata.videoIndex
val MediaItem.subtitleIndex get() = mediaMetadata.subtitleIndex
val MediaItem.video get() = mediaMetadata.video
val MediaItem.isLiked get() = mediaMetadata.isLiked

val MediaItem.audioStreamable get() = track.audioStreamables[audioIndex]
val MediaItem.videoStreamable get() = track.videoStreamables.getOrNull(videoIndex)

private fun Streamable.SubtitleType.toMimeType() = when (this) {
Streamable.SubtitleType.VTT -> MimeTypes.TEXT_VTT
Streamable.SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
Streamable.SubtitleType.ASS -> MimeTypes.TEXT_SSA
}
}
Loading

0 comments on commit 7727467

Please sign in to comment.