feat(android): PlayerFactory wraps ExoPlayer in ForwardingPlayer + drop events
android / Build + lint + test (push) Failing after 1m35s
android / Build + lint + test (push) Failing after 1m35s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package com.fabledsword.minstrel.player
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.media3.common.AudioAttributes
|
import androidx.media3.common.AudioAttributes
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.database.StandaloneDatabaseProvider
|
import androidx.media3.database.StandaloneDatabaseProvider
|
||||||
import androidx.media3.datasource.cache.CacheDataSink
|
import androidx.media3.datasource.cache.CacheDataSink
|
||||||
import androidx.media3.datasource.cache.CacheDataSource
|
import androidx.media3.datasource.cache.CacheDataSource
|
||||||
@@ -12,14 +13,19 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource
|
|||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
import com.fabledsword.minstrel.cache.audiocache.CacheConfig
|
import com.fabledsword.minstrel.cache.audiocache.CacheConfig
|
||||||
|
import com.fabledsword.minstrel.player.output.ActiveUpnpHolder
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the process-singleton ExoPlayer with our shared OkHttp +
|
* Builds the process-singleton player with our shared OkHttp +
|
||||||
* SimpleCache chain. The MinstrelPlayerService (Phase 6.2) calls
|
* SimpleCache chain. The MinstrelPlayerService (Phase 6.2) calls
|
||||||
* `build()` once during onCreate.
|
* `build()` once during onCreate.
|
||||||
*
|
*
|
||||||
@@ -31,12 +37,22 @@ import javax.inject.Singleton
|
|||||||
* (sizeBytes cap = rollingCap); our policy layer in the worker layers
|
* (sizeBytes cap = rollingCap); our policy layer in the worker layers
|
||||||
* the 2-bucket protection on top by feeding `removeSpan` only for
|
* the 2-bucket protection on top by feeding `removeSpan` only for
|
||||||
* unprotected tracks.
|
* unprotected tracks.
|
||||||
|
*
|
||||||
|
* `build()` returns a [MinstrelForwardingPlayer] wrapping the internal
|
||||||
|
* ExoPlayer. When the UPnP route drops (3 consecutive poll failures or
|
||||||
|
* a SOAP failure), [dropEvents] emits the route name so the NowPlaying
|
||||||
|
* surface can show a snackbar. The MutableSharedFlow uses DROP_OLDEST
|
||||||
|
* with capacity=1 so a burst of failures during a single tear-down
|
||||||
|
* surfaces as one event rather than queueing N.
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class PlayerFactory @Inject constructor(
|
class PlayerFactory @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val cacheConfig: CacheConfig,
|
private val cacheConfig: CacheConfig,
|
||||||
|
private val activeUpnpHolder: ActiveUpnpHolder,
|
||||||
|
private val streamTokens: StreamTokenProvider,
|
||||||
|
private val remoteState: RemotePlayerState,
|
||||||
) {
|
) {
|
||||||
private val cacheDir: File = File(context.cacheDir, "audio_cache").apply { mkdirs() }
|
private val cacheDir: File = File(context.cacheDir, "audio_cache").apply { mkdirs() }
|
||||||
|
|
||||||
@@ -46,7 +62,28 @@ class PlayerFactory @Inject constructor(
|
|||||||
StandaloneDatabaseProvider(context),
|
StandaloneDatabaseProvider(context),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun build(): ExoPlayer {
|
// MutableSharedFlow with extraBufferCapacity=1 + DROP_OLDEST so a burst
|
||||||
|
// of drop events (rapid SOAP failures during a single tear-down) surfaces
|
||||||
|
// as one snackbar rather than queueing N.
|
||||||
|
private val dropEventsInternal = MutableSharedFlow<String>(
|
||||||
|
replay = 0,
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||||
|
)
|
||||||
|
val dropEvents: SharedFlow<String> = dropEventsInternal.asSharedFlow()
|
||||||
|
|
||||||
|
fun build(): Player {
|
||||||
|
val exo = buildExoPlayer()
|
||||||
|
return MinstrelForwardingPlayer(
|
||||||
|
delegate = exo,
|
||||||
|
holder = activeUpnpHolder,
|
||||||
|
remoteState = remoteState,
|
||||||
|
tokens = streamTokens,
|
||||||
|
onDrop = { name -> emitDrop(name) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildExoPlayer(): ExoPlayer {
|
||||||
val httpDataSource = OkHttpDataSource.Factory(okHttpClient)
|
val httpDataSource = OkHttpDataSource.Factory(okHttpClient)
|
||||||
val cacheDataSource = CacheDataSource.Factory()
|
val cacheDataSource = CacheDataSource.Factory()
|
||||||
.setCache(simpleCache)
|
.setCache(simpleCache)
|
||||||
@@ -71,4 +108,8 @@ class PlayerFactory @Inject constructor(
|
|||||||
.setHandleAudioBecomingNoisy(true)
|
.setHandleAudioBecomingNoisy(true)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun emitDrop(routeName: String) {
|
||||||
|
dropEventsInternal.tryEmit(routeName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user