mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
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:
@@ -38,7 +38,9 @@ data class ListQuery(
|
|||||||
data class StreamQuality(
|
data class StreamQuality(
|
||||||
val maxBitrate: Int = 0,
|
val maxBitrate: Int = 0,
|
||||||
val format: String? = null,
|
val format: String? = null,
|
||||||
)
|
) {
|
||||||
|
val transcodes: Boolean get() = maxBitrate > 0 || !format.isNullOrEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
private val Context.preferences: DataStore<Preferences> by preferencesDataStore("user_prefs")
|
private val Context.preferences: DataStore<Preferences> by preferencesDataStore("user_prefs")
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ fun declaredStreamLength(
|
|||||||
quality: StreamQuality,
|
quality: StreamQuality,
|
||||||
): Long? {
|
): Long? {
|
||||||
if (durationMs == null || durationMs <= 0) return null
|
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
|
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
|
return durationMs / 1000 * bps / 8 * 5 / 4 + OVERHEAD_BYTES
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.Bundle
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.MediaItem.ClippingConfiguration
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
@@ -142,12 +143,20 @@ class Media3PlayerHandle(
|
|||||||
item.durationMs?.let { putLong(EXTRA_DURATION_MS, it) }
|
item.durationMs?.let { putLong(EXTRA_DURATION_MS, it) }
|
||||||
},
|
},
|
||||||
).build()
|
).build()
|
||||||
return MediaItem
|
val builder =
|
||||||
|
MediaItem
|
||||||
.Builder()
|
.Builder()
|
||||||
.setMediaId(item.id)
|
.setMediaId(item.id)
|
||||||
.setUri(sourceRepository.streamUri(item.id, item.durationMs))
|
.setUri(sourceRepository.streamUri(item.id, item.durationMs))
|
||||||
.setMediaMetadata(metadata)
|
.setMediaMetadata(metadata)
|
||||||
.build()
|
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 =
|
private fun toQueueItem(mediaItem: MediaItem): QueueItem =
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.media3.common.AudioAttributes
|
|||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.ForwardingPlayer
|
import androidx.media3.common.ForwardingPlayer
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.util.ExperimentalApi
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
@@ -25,7 +26,7 @@ class PlaybackService : MediaSessionService() {
|
|||||||
private var session: MediaSession? = null
|
private var session: MediaSession? = null
|
||||||
private var artworkLoader: CoverArtBitmapLoader? = null
|
private var artworkLoader: CoverArtBitmapLoader? = null
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class, ExperimentalApi::class)
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
val exoPlayer =
|
val exoPlayer =
|
||||||
@@ -40,7 +41,9 @@ class PlaybackService : MediaSessionService() {
|
|||||||
true,
|
true,
|
||||||
).setHandleAudioBecomingNoisy(true)
|
).setHandleAudioBecomingNoisy(true)
|
||||||
.setMediaSourceFactory(
|
.setMediaSourceFactory(
|
||||||
DefaultMediaSourceFactory(this).setDataSourceFactory(
|
DefaultMediaSourceFactory(this)
|
||||||
|
.setEnableClippingInMediaPeriod(true)
|
||||||
|
.setDataSourceFactory(
|
||||||
KnownLengthDataSourceFactory(OkHttpDataSource.Factory(streamingClient())),
|
KnownLengthDataSourceFactory(OkHttpDataSource.Factory(streamingClient())),
|
||||||
),
|
),
|
||||||
).build()
|
).build()
|
||||||
|
|||||||
Reference in New Issue
Block a user