From e43a2b6fe56d558c5f45786e5b14874b201e6dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Garc=C3=ADa?= <55400857+jaime-grj@users.noreply.github.com> Date: Fri, 5 Sep 2025 04:46:01 +0200 Subject: [PATCH 1/4] fix: Resolve playback issues with live radio MPEG & HLS streams --- .../tempo/util/DownloadUtil.java | 54 +++++++------- .../service/DynamicMediaSourceFactory.kt | 73 +++++++++++++++++++ .../tempo/service/MediaService.kt | 7 +- 3 files changed, 103 insertions(+), 31 deletions(-) create mode 100644 app/src/notquitemy/java/com/cappielloantonio/tempo/service/DynamicMediaSourceFactory.kt diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java index a8cafc4a..3b6babfe 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java @@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.util; import android.app.Notification; import android.content.Context; +import android.net.Uri; import androidx.core.app.NotificationCompat; import androidx.media3.common.util.UnstableApi; @@ -78,35 +79,36 @@ public final class DownloadUtil { return httpDataSourceFactory; } - public static synchronized DataSource.Factory getDataSourceFactory(Context context) { - if (dataSourceFactory == null) { - context = context.getApplicationContext(); - - DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory()); - - if (Preferences.getStreamingCacheSize() > 0) { - CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory() - .setCache(getStreamingCache(context)) - .setUpstreamDataSourceFactory(upstreamFactory); - - ResolvingDataSource.Factory resolvingFactory = new ResolvingDataSource.Factory( - new StreamingCacheDataSource.Factory(streamCacheFactory), - dataSpec -> { - DataSpec.Builder builder = dataSpec.buildUpon(); - builder.setFlags(dataSpec.flags & ~DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN); - return builder.build(); - } - ); - - dataSourceFactory = buildReadOnlyCacheDataSource(resolvingFactory, getDownloadCache(context)); - } else { - dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context)); - } - } - + public static synchronized DataSource.Factory getUpstreamDataSourceFactory(Context context) { + DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory()); + dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context)); return dataSourceFactory; } + public static synchronized DataSource.Factory getCacheDataSourceFactory(Context context) { + CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory() + .setCache(getStreamingCache(context)) + .setUpstreamDataSourceFactory(getUpstreamDataSourceFactory(context)); + + ResolvingDataSource.Factory resolvingFactory = new ResolvingDataSource.Factory( + new StreamingCacheDataSource.Factory(streamCacheFactory), + dataSpec -> { + DataSpec.Builder builder = dataSpec.buildUpon(); + builder.setFlags(dataSpec.flags & ~DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN); + return builder.build(); + } + ); + dataSourceFactory = buildReadOnlyCacheDataSource(resolvingFactory, getDownloadCache(context)); + return dataSourceFactory; + } + + public static boolean shouldBypassCache(Uri uri) { + if (uri == null) return true; + String url = uri.toString(); + String mainServer = Preferences.getServer(); + return mainServer != null && !url.startsWith(mainServer); + } + public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(Context context) { if (downloadNotificationHelper == null) { downloadNotificationHelper = new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID); diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/DynamicMediaSourceFactory.kt b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/DynamicMediaSourceFactory.kt new file mode 100644 index 00000000..8c8df4d3 --- /dev/null +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/DynamicMediaSourceFactory.kt @@ -0,0 +1,73 @@ +package com.cappielloantonio.tempo.service + +import android.content.Context +import android.net.Uri +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource +import androidx.media3.exoplayer.drm.DrmSessionManagerProvider +import androidx.media3.exoplayer.hls.HlsMediaSource +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy +import androidx.media3.extractor.DefaultExtractorsFactory +import androidx.media3.extractor.ExtractorsFactory +import com.cappielloantonio.tempo.util.DownloadUtil +import com.cappielloantonio.tempo.util.Preferences + +@UnstableApi +class DynamicMediaSourceFactory( + private val context: Context +) : MediaSource.Factory { + + override fun createMediaSource(mediaItem: MediaItem): MediaSource { + val uri: Uri = mediaItem.localConfiguration?.uri ?: mediaItem.requestMetadata.mediaUri + ?: throw IllegalArgumentException("MediaItem must contain a valid URI") + + val streamingCacheSize = Preferences.getStreamingCacheSize() + val bypassCache = DownloadUtil.shouldBypassCache(uri) + + val useUpstream = when { + streamingCacheSize.toInt() == 0 -> true + streamingCacheSize > 0 && bypassCache -> true + streamingCacheSize > 0 && !bypassCache -> false + else -> true + } + + val dataSourceFactory: DataSource.Factory = if (useUpstream) { + DownloadUtil.getUpstreamDataSourceFactory(context) + } else { + DownloadUtil.getCacheDataSourceFactory(context) + } + + return when { + mediaItem.localConfiguration?.mimeType == MimeTypes.APPLICATION_M3U8 || + uri.toString().endsWith(".m3u8") -> { + HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem) + } + + else -> { + val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory() + ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) + .createMediaSource(mediaItem) + } + } + } + + override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory { + TODO("Not yet implemented") + } + + override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory { + TODO("Not yet implemented") + } + + override fun getSupportedTypes(): IntArray { + return intArrayOf( + C.CONTENT_TYPE_HLS, + C.CONTENT_TYPE_OTHER + ) + } +} \ No newline at end of file diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt index 92c4b9b3..c78b660c 100644 --- a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -10,7 +10,6 @@ import androidx.media3.common.* import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.TrackGroupArray import androidx.media3.exoplayer.trackselection.TrackSelectionArray import androidx.media3.session.* @@ -19,6 +18,7 @@ import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.Constants import com.cappielloantonio.tempo.util.DownloadUtil +import com.cappielloantonio.tempo.service.DynamicMediaSourceFactory import com.cappielloantonio.tempo.util.Preferences import com.cappielloantonio.tempo.util.ReplayGainUtil import com.google.common.collect.ImmutableList @@ -186,7 +186,7 @@ class MediaService : MediaLibraryService() { private fun initializePlayer() { player = ExoPlayer.Builder(this) .setRenderersFactory(getRenderersFactory()) - .setMediaSourceFactory(getMediaSourceFactory()) + .setMediaSourceFactory(DynamicMediaSourceFactory(this)) .setAudioAttributes(AudioAttributes.DEFAULT, true) .setHandleAudioBecomingNoisy(true) .setWakeMode(C.WAKE_MODE_NETWORK) @@ -346,7 +346,4 @@ class MediaService : MediaLibraryService() { } private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) - - private fun getMediaSourceFactory() = - DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this)) } \ No newline at end of file From bd753f44897b676aa1356ea72c8c0cd6636a8caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Garc=C3=ADa?= <55400857+jaime-grj@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:19:47 +0200 Subject: [PATCH 2/4] fix: Use defined media type for live radio detection, relocate DynamicMediaSourceFactory --- .../cappielloantonio/tempo/util/DownloadUtil.java | 8 -------- .../tempo/util}/DynamicMediaSourceFactory.kt | 12 ++++-------- .../cappielloantonio/tempo/service/MediaService.kt | 2 +- 3 files changed, 5 insertions(+), 17 deletions(-) rename app/src/{notquitemy/java/com/cappielloantonio/tempo/service => main/java/com/cappielloantonio/tempo/util}/DynamicMediaSourceFactory.kt (83%) diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java index 3b6babfe..238b4136 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java @@ -2,7 +2,6 @@ package com.cappielloantonio.tempo.util; import android.app.Notification; import android.content.Context; -import android.net.Uri; import androidx.core.app.NotificationCompat; import androidx.media3.common.util.UnstableApi; @@ -102,13 +101,6 @@ public final class DownloadUtil { return dataSourceFactory; } - public static boolean shouldBypassCache(Uri uri) { - if (uri == null) return true; - String url = uri.toString(); - String mainServer = Preferences.getServer(); - return mainServer != null && !url.startsWith(mainServer); - } - public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(Context context) { if (downloadNotificationHelper == null) { downloadNotificationHelper = new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID); diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/DynamicMediaSourceFactory.kt b/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt similarity index 83% rename from app/src/notquitemy/java/com/cappielloantonio/tempo/service/DynamicMediaSourceFactory.kt rename to app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt index 8c8df4d3..575cf837 100644 --- a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/DynamicMediaSourceFactory.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt @@ -1,7 +1,6 @@ -package com.cappielloantonio.tempo.service +package com.cappielloantonio.tempo.util import android.content.Context -import android.net.Uri import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes @@ -14,8 +13,6 @@ import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy import androidx.media3.extractor.DefaultExtractorsFactory import androidx.media3.extractor.ExtractorsFactory -import com.cappielloantonio.tempo.util.DownloadUtil -import com.cappielloantonio.tempo.util.Preferences @UnstableApi class DynamicMediaSourceFactory( @@ -23,11 +20,10 @@ class DynamicMediaSourceFactory( ) : MediaSource.Factory { override fun createMediaSource(mediaItem: MediaItem): MediaSource { - val uri: Uri = mediaItem.localConfiguration?.uri ?: mediaItem.requestMetadata.mediaUri - ?: throw IllegalArgumentException("MediaItem must contain a valid URI") + val mediaType: String? = mediaItem.mediaMetadata.extras?.getString("type", "") val streamingCacheSize = Preferences.getStreamingCacheSize() - val bypassCache = DownloadUtil.shouldBypassCache(uri) + val bypassCache = mediaType == Constants.MEDIA_TYPE_RADIO val useUpstream = when { streamingCacheSize.toInt() == 0 -> true @@ -44,7 +40,7 @@ class DynamicMediaSourceFactory( return when { mediaItem.localConfiguration?.mimeType == MimeTypes.APPLICATION_M3U8 || - uri.toString().endsWith(".m3u8") -> { + mediaItem.localConfiguration?.uri.toString().endsWith(".m3u8") -> { HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem) } diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt index c78b660c..d595afe7 100644 --- a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -18,7 +18,7 @@ import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.Constants import com.cappielloantonio.tempo.util.DownloadUtil -import com.cappielloantonio.tempo.service.DynamicMediaSourceFactory +import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory import com.cappielloantonio.tempo.util.Preferences import com.cappielloantonio.tempo.util.ReplayGainUtil import com.google.common.collect.ImmutableList From fab18c130e4e63f07a32358e591af25e06fd473b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Garc=C3=ADa?= <55400857+jaime-grj@users.noreply.github.com> Date: Fri, 5 Sep 2025 21:29:11 +0200 Subject: [PATCH 3/4] fix: Support HLS (.m3u8) streams with parameters in URL --- .../cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt b/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt index 575cf837..31dc172a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt @@ -40,7 +40,7 @@ class DynamicMediaSourceFactory( return when { mediaItem.localConfiguration?.mimeType == MimeTypes.APPLICATION_M3U8 || - mediaItem.localConfiguration?.uri.toString().endsWith(".m3u8") -> { + mediaItem.localConfiguration?.uri?.lastPathSegment?.endsWith(".m3u8", ignoreCase = true) == true -> { HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem) } From d4cb6c5c9a6a08ed7ad7d57b584671828294c106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Garc=C3=ADa?= <55400857+jaime-grj@users.noreply.github.com> Date: Sat, 6 Sep 2025 00:20:35 +0200 Subject: [PATCH 4/4] fix: Update MediaService in all build variants --- .../com/cappielloantonio/tempo/service/MediaService.kt | 7 ++----- .../com/cappielloantonio/tempo/service/MediaService.kt | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt index 2391a2bb..e46a140a 100644 --- a/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -14,13 +14,13 @@ import androidx.media3.common.Tracks import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession.ControllerInfo import com.cappielloantonio.tempo.repository.AutomotiveRepository import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.Constants import com.cappielloantonio.tempo.util.DownloadUtil +import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory import com.cappielloantonio.tempo.util.Preferences import com.cappielloantonio.tempo.util.ReplayGainUtil import com.google.android.gms.cast.framework.CastContext @@ -74,7 +74,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { private fun initializePlayer() { player = ExoPlayer.Builder(this) .setRenderersFactory(getRenderersFactory()) - .setMediaSourceFactory(getMediaSourceFactory()) + .setMediaSourceFactory(DynamicMediaSourceFactory(this)) .setAudioAttributes(AudioAttributes.DEFAULT, true) .setHandleAudioBecomingNoisy(true) .setWakeMode(C.WAKE_MODE_NETWORK) @@ -216,9 +216,6 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) - private fun getMediaSourceFactory() = - DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this)) - override fun onCastSessionAvailable() { setPlayer(player, castPlayer) } diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt index 2391a2bb..e46a140a 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -14,13 +14,13 @@ import androidx.media3.common.Tracks import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession.ControllerInfo import com.cappielloantonio.tempo.repository.AutomotiveRepository import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.Constants import com.cappielloantonio.tempo.util.DownloadUtil +import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory import com.cappielloantonio.tempo.util.Preferences import com.cappielloantonio.tempo.util.ReplayGainUtil import com.google.android.gms.cast.framework.CastContext @@ -74,7 +74,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { private fun initializePlayer() { player = ExoPlayer.Builder(this) .setRenderersFactory(getRenderersFactory()) - .setMediaSourceFactory(getMediaSourceFactory()) + .setMediaSourceFactory(DynamicMediaSourceFactory(this)) .setAudioAttributes(AudioAttributes.DEFAULT, true) .setHandleAudioBecomingNoisy(true) .setWakeMode(C.WAKE_MODE_NETWORK) @@ -216,9 +216,6 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) - private fun getMediaSourceFactory() = - DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this)) - override fun onCastSessionAvailable() { setPlayer(player, castPlayer) }