mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
playback: show notification progress for unknown-length streams
Media3 marks a progressive stream live when the data source reports no length and the extractor no duration, and a live item makes the media session publish PLAYBACK_POSITION_UNKNOWN, so the notification progress never advances even though the item carries the library duration. The app only declares a stream length for a transcode of a track that has one in a length-estimable format (`declaredStreamLength`), so a chunked response stays live for an Opus/Ogg/Vorbis transcode (a length estimate breaks the end-seek they rely on), for a transcode of a track whose library duration is missing, and for a direct play the server does not give a Content-Length. The now playing screen shows progress in all of those because it falls back to the metadata duration; the notification had nothing to advance. Wrap the ExoPlayer in a ForwardingPlayer that reports an item with a declared duration as finite. The session then publishes the real position and the metadata duration, so the notification shows progress for every unknown-length stream, not just Opus. The stream is still not seekable; only the reported liveness changes.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package com.subtracks.playback
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.ForwardingPlayer
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
/**
|
||||
* Reports a track whose duration is known from its metadata as finite rather than live.
|
||||
*
|
||||
* Media3 marks a progressive stream live when the data source reports no length and the extractor
|
||||
* no duration. The app declares a length only for a transcode of a track that has one in a
|
||||
* length-estimable format, so a chunked response stays live for an Opus/Ogg/Vorbis transcode, for
|
||||
* a transcode of a track whose library duration is missing, and for a direct play the server does
|
||||
* not give a Content-Length. A live item makes the media session publish no playback position, so
|
||||
* the notification loses its progress even though the item carries a real duration. The stream is
|
||||
* still not seekable; this only restores the position the session reports.
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
internal class KnownDurationPlayer(
|
||||
player: Player,
|
||||
) : ForwardingPlayer(player) {
|
||||
override fun isCurrentMediaItemLive(): Boolean = if (declaredDurationMs() != null) false else super.isCurrentMediaItemLive()
|
||||
|
||||
private fun declaredDurationMs(): Long? =
|
||||
currentMediaItem
|
||||
?.mediaMetadata
|
||||
?.durationMs
|
||||
?.takeIf { it > 0 }
|
||||
}
|
||||
@@ -130,6 +130,7 @@ class PlaybackService : MediaSessionService() {
|
||||
}
|
||||
},
|
||||
)
|
||||
val player = KnownDurationPlayer(exoPlayer)
|
||||
val loader =
|
||||
CoverArtBitmapLoader(
|
||||
applicationContext,
|
||||
@@ -139,7 +140,7 @@ class PlaybackService : MediaSessionService() {
|
||||
artworkLoader = loader
|
||||
session =
|
||||
MediaSession
|
||||
.Builder(this, exoPlayer)
|
||||
.Builder(this, player)
|
||||
.setSessionActivity(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.subtracks.playback
|
||||
|
||||
import android.os.Looper
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.SimpleBasePlayer
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class KnownDurationPlayerTest {
|
||||
private fun player(
|
||||
declaredDurationMs: Long?,
|
||||
live: Boolean = true,
|
||||
): Player =
|
||||
object : SimpleBasePlayer(Looper.getMainLooper()) {
|
||||
override fun getState(): State {
|
||||
val metadata =
|
||||
MediaMetadata
|
||||
.Builder()
|
||||
.setTitle("Track")
|
||||
.setDurationMs(declaredDurationMs)
|
||||
.build()
|
||||
val item =
|
||||
MediaItemData
|
||||
.Builder("track")
|
||||
.setMediaItem(
|
||||
MediaItem
|
||||
.Builder()
|
||||
.setMediaId("s1")
|
||||
.setMediaMetadata(metadata)
|
||||
.build(),
|
||||
).setDurationUs(PLAYER_DURATION_US)
|
||||
.setLiveConfiguration(if (live) MediaItem.LiveConfiguration.UNSET else null)
|
||||
.build()
|
||||
return State
|
||||
.Builder()
|
||||
.setPlaylist(listOf(item))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aLiveTrackWithADeclaredDurationIsReportedFinite() {
|
||||
val player = player(declaredDurationMs = 100_000L)
|
||||
|
||||
assertFalse(KnownDurationPlayer(player).isCurrentMediaItemLive())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aLiveTrackWithoutADeclaredDurationStaysLive() {
|
||||
val player = player(declaredDurationMs = null)
|
||||
|
||||
assertTrue(KnownDurationPlayer(player).isCurrentMediaItemLive())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aFiniteTrackWithADeclaredDurationStaysFinite() {
|
||||
val player = player(declaredDurationMs = 100_000L, live = false)
|
||||
|
||||
assertFalse(KnownDurationPlayer(player).isCurrentMediaItemLive())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theWrappedDurationIsThePlayersNotTheMetadata() {
|
||||
val player = player(declaredDurationMs = 100_000L)
|
||||
|
||||
assertEquals(PLAYER_DURATION_MS, KnownDurationPlayer(player).getDuration())
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PLAYER_DURATION_MS = 200_000L
|
||||
const val PLAYER_DURATION_US = PLAYER_DURATION_MS * 1000
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user