clip transcoded tracks to their real duration

The declared upper-bound length stopped the stall where Media3 saw no seek map duration, but it also made the track look longer than the audio: the player reached the last sample still short of the declared end and waited for data that never came, so it never advanced. Clip each transcoded item to the library duration, which Media3 applies through ClippingMediaSource, so the timeline ends where the audio ends and the next track advances. The declared length still makes the stream on-demand (seekable) and never bounds reads below the real size.
This commit is contained in:
austinried
2026-10-04 00:30:00 +00:00
parent 22feb2372d
commit ebaa486126
4 changed files with 26 additions and 12 deletions
@@ -38,7 +38,9 @@ data class ListQuery(
data class StreamQuality(
val maxBitrate: Int = 0,
val format: String? = null,
)
) {
val transcodes: Boolean get() = maxBitrate > 0 || !format.isNullOrEmpty()
}
private val Context.preferences: DataStore<Preferences> by preferencesDataStore("user_prefs")
@@ -13,7 +13,7 @@ fun declaredStreamLength(
quality: StreamQuality,
): Long? {
if (durationMs == null || durationMs <= 0) return null
if (quality.maxBitrate <= 0 && quality.format.isNullOrEmpty()) return null
if (!quality.transcodes) return null
val bps = if (quality.maxBitrate > 0) maxOf(quality.maxBitrate * 1000L, MIN_TRANSCODE_BPS) else UNCAPPED_BPS
return durationMs / 1000 * bps / 8 * 5 / 4 + OVERHEAD_BYTES
}
@@ -4,6 +4,7 @@ import android.os.Bundle
import android.util.Log
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.ClippingConfiguration
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
@@ -142,12 +143,20 @@ class Media3PlayerHandle(
item.durationMs?.let { putLong(EXTRA_DURATION_MS, it) }
},
).build()
return MediaItem
.Builder()
.setMediaId(item.id)
.setUri(sourceRepository.streamUri(item.id, item.durationMs))
.setMediaMetadata(metadata)
.build()
val builder =
MediaItem
.Builder()
.setMediaId(item.id)
.setUri(sourceRepository.streamUri(item.id, item.durationMs))
.setMediaMetadata(metadata)
if (sourceRepository.quality.value.transcodes) {
item.durationMs?.takeIf { it > 0 }?.let { duration ->
builder.setClippingConfiguration(
ClippingConfiguration.Builder().setEndPositionMs(duration).build(),
)
}
}
return builder.build()
}
private fun toQueueItem(mediaItem: MediaItem): QueueItem =
@@ -9,6 +9,7 @@ import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.ForwardingPlayer
import androidx.media3.common.Player
import androidx.media3.common.util.ExperimentalApi
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
@@ -25,7 +26,7 @@ class PlaybackService : MediaSessionService() {
private var session: MediaSession? = null
private var artworkLoader: CoverArtBitmapLoader? = null
@OptIn(UnstableApi::class)
@OptIn(UnstableApi::class, ExperimentalApi::class)
override fun onCreate() {
super.onCreate()
val exoPlayer =
@@ -40,9 +41,11 @@ class PlaybackService : MediaSessionService() {
true,
).setHandleAudioBecomingNoisy(true)
.setMediaSourceFactory(
DefaultMediaSourceFactory(this).setDataSourceFactory(
KnownLengthDataSourceFactory(OkHttpDataSource.Factory(streamingClient())),
),
DefaultMediaSourceFactory(this)
.setEnableClippingInMediaPeriod(true)
.setDataSourceFactory(
KnownLengthDataSourceFactory(OkHttpDataSource.Factory(streamingClient())),
),
).build()
val player = MetadataDurationPlayer(exoPlayer)
val loader =