fix: Resolve playback issues with live radio MPEG & HLS streams

This commit is contained in:
Jaime García 2025-09-05 04:46:01 +02:00
parent c62d2ace4d
commit e43a2b6fe5
No known key found for this signature in database
GPG key ID: BC4E5F71A71BDA5B
3 changed files with 103 additions and 31 deletions

View file

@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.util;
import android.app.Notification; import android.app.Notification;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
@ -78,35 +79,36 @@ public final class DownloadUtil {
return httpDataSourceFactory; return httpDataSourceFactory;
} }
public static synchronized DataSource.Factory getDataSourceFactory(Context context) { public static synchronized DataSource.Factory getUpstreamDataSourceFactory(Context context) {
if (dataSourceFactory == null) { DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory());
context = context.getApplicationContext(); dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
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));
}
}
return dataSourceFactory; 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) { public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(Context context) {
if (downloadNotificationHelper == null) { if (downloadNotificationHelper == null) {
downloadNotificationHelper = new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID); downloadNotificationHelper = new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID);

View file

@ -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
)
}
}

View file

@ -10,7 +10,6 @@ import androidx.media3.common.*
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.TrackGroupArray import androidx.media3.exoplayer.source.TrackGroupArray
import androidx.media3.exoplayer.trackselection.TrackSelectionArray import androidx.media3.exoplayer.trackselection.TrackSelectionArray
import androidx.media3.session.* import androidx.media3.session.*
@ -19,6 +18,7 @@ import com.cappielloantonio.tempo.R
import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.ui.activity.MainActivity
import com.cappielloantonio.tempo.util.Constants import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.DownloadUtil import com.cappielloantonio.tempo.util.DownloadUtil
import com.cappielloantonio.tempo.service.DynamicMediaSourceFactory
import com.cappielloantonio.tempo.util.Preferences import com.cappielloantonio.tempo.util.Preferences
import com.cappielloantonio.tempo.util.ReplayGainUtil import com.cappielloantonio.tempo.util.ReplayGainUtil
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
@ -186,7 +186,7 @@ class MediaService : MediaLibraryService() {
private fun initializePlayer() { private fun initializePlayer() {
player = ExoPlayer.Builder(this) player = ExoPlayer.Builder(this)
.setRenderersFactory(getRenderersFactory()) .setRenderersFactory(getRenderersFactory())
.setMediaSourceFactory(getMediaSourceFactory()) .setMediaSourceFactory(DynamicMediaSourceFactory(this))
.setAudioAttributes(AudioAttributes.DEFAULT, true) .setAudioAttributes(AudioAttributes.DEFAULT, true)
.setHandleAudioBecomingNoisy(true) .setHandleAudioBecomingNoisy(true)
.setWakeMode(C.WAKE_MODE_NETWORK) .setWakeMode(C.WAKE_MODE_NETWORK)
@ -346,7 +346,4 @@ class MediaService : MediaLibraryService() {
} }
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
private fun getMediaSourceFactory() =
DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this))
} }