make exoPlayer a StateFlow that can be null

This commit is contained in:
Christian Schabesberger 2024-09-11 16:23:14 +02:00
parent 2e50634b50
commit 4fb5d46b2d
6 changed files with 199 additions and 188 deletions

View File

@ -20,11 +20,8 @@
package net.newpipe.newplayer
import android.app.Application
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
@ -49,7 +46,7 @@ interface NewPlayer {
// preferences
val preferredStreamVariants: List<String>
val internalPlayer: Player
val exoPlayer: StateFlow<Player?>
var playWhenReady: Boolean
val duration: Long
val bufferedPercentage: Int
@ -81,33 +78,4 @@ interface NewPlayer {
fun selectChapter(index: Int)
fun playStream(item: String, streamVariant: String, playMode: PlayMode)
fun release()
data class Builder(val app: Application, val repository: MediaRepository) {
private var mediaSourceFactory: MediaSource.Factory? = null
private var preferredStreamVariants: List<String> = emptyList()
fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory): Builder {
this.mediaSourceFactory = mediaSourceFactory
return this
}
fun setPreferredStreamVariants(preferredStreamVariants: List<String>): Builder {
this.preferredStreamVariants = preferredStreamVariants
return this
}
fun build(): NewPlayer {
val exoPlayerBuilder = ExoPlayer.Builder(app)
mediaSourceFactory?.let {
exoPlayerBuilder.setMediaSourceFactory(it)
}
return NewPlayerImpl(
app = app,
internalPlayer = exoPlayerBuilder.build(),
repository = repository,
preferredStreamVariants = preferredStreamVariants,
)
}
}
}

View File

@ -22,13 +22,12 @@ package net.newpipe.newplayer
import android.app.Application
import android.content.ComponentName
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.MoreExecutors
@ -50,11 +49,13 @@ private const val TAG = "NewPlayerImpl"
class NewPlayerImpl(
val app: Application,
override val internalPlayer: Player,
override val preferredStreamVariants: List<String>,
private val repository: MediaRepository
private val repository: MediaRepository,
override val preferredStreamVariants: List<String> = emptyList()
) : NewPlayer {
private val mutableExoPlayer = MutableStateFlow<ExoPlayer?>(null)
override val exoPlayer = mutableExoPlayer.asStateFlow()
private var playerScope = CoroutineScope(Dispatchers.Main + Job())
private var uniqueIdToIdLookup = HashMap<Long, String>()
@ -66,12 +67,12 @@ class NewPlayerImpl(
override val errorFlow = mutableErrorFlow.asSharedFlow()
override val bufferedPercentage: Int
get() = internalPlayer.bufferedPercentage
get() = exoPlayer.value?.bufferedPercentage ?: 0
override var currentPosition: Long
get() = internalPlayer.currentPosition
get() = exoPlayer.value?.currentPosition ?: 0
set(value) {
internalPlayer.seekTo(value)
exoPlayer.value?.seekTo(value)
}
override var fastSeekAmountSec: Int = 10
@ -79,23 +80,23 @@ class NewPlayerImpl(
override var playBackMode = MutableStateFlow(PlayMode.IDLE)
override var shuffle: Boolean
get() = internalPlayer.shuffleModeEnabled
get() = exoPlayer.value?.shuffleModeEnabled ?: false
set(value) {
internalPlayer.shuffleModeEnabled = value
exoPlayer.value?.shuffleModeEnabled = value
}
override var repeatMode: RepeatMode
get() = when (internalPlayer.repeatMode) {
get() = when (exoPlayer.value?.repeatMode) {
Player.REPEAT_MODE_OFF -> RepeatMode.DONT_REPEAT
Player.REPEAT_MODE_ALL -> RepeatMode.REPEAT_ALL
Player.REPEAT_MODE_ONE -> RepeatMode.REPEAT_ONE
else -> throw NewPlayerException("Unknown Repeatmode option returned by ExoPlayer: ${internalPlayer.repeatMode}")
else -> throw NewPlayerException("Unknown Repeatmode option returned by ExoPlayer: ${exoPlayer.value?.repeatMode}")
}
set(value) {
when (value) {
RepeatMode.DONT_REPEAT -> internalPlayer.repeatMode = Player.REPEAT_MODE_OFF
RepeatMode.REPEAT_ALL -> internalPlayer.repeatMode = Player.REPEAT_MODE_ALL
RepeatMode.REPEAT_ONE -> internalPlayer.repeatMode = Player.REPEAT_MODE_ONE
RepeatMode.DONT_REPEAT -> exoPlayer.value?.repeatMode = Player.REPEAT_MODE_OFF
RepeatMode.REPEAT_ALL -> exoPlayer.value?.repeatMode = Player.REPEAT_MODE_ALL
RepeatMode.REPEAT_ONE -> exoPlayer.value?.repeatMode = Player.REPEAT_MODE_ONE
}
}
@ -105,13 +106,13 @@ class NewPlayerImpl(
override var playWhenReady: Boolean
set(value) {
internalPlayer.playWhenReady = value
exoPlayer.value?.playWhenReady = value
}
get() = internalPlayer.playWhenReady
get() = exoPlayer.value?.playWhenReady ?: false
override val duration: Long
get() = internalPlayer.duration
get() = exoPlayer.value?.duration ?: 0
private val mutablePlaylist = MutableStateFlow<List<MediaItem>>(emptyList())
override val playlist: StateFlow<List<MediaItem>> =
@ -124,19 +125,20 @@ class NewPlayerImpl(
override val currentChapters: StateFlow<List<Chapter>> = mutableCurrentChapter.asStateFlow()
override var currentlyPlayingPlaylistItem: Int
get() = internalPlayer.currentMediaItemIndex
get() = exoPlayer.value?.currentMediaItemIndex ?: -1
set(value) {
assert(value in 0..<playlist.value.size) {
throw NewPlayerException("Playlist item selection out of bound: selected item index: $value, available chapters: ${playlist.value.size}")
}
internalPlayer.seekTo(value, 0)
exoPlayer.value?.seekTo(value, 0)
}
init {
internalPlayer.addListener(object : Player.Listener {
private fun setupNewExoplayer() {
val newExoPlayer = ExoPlayer.Builder(app).build()
newExoPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
launchJobAndCollectError {
val item = internalPlayer.currentMediaItem?.mediaId
val item = newExoPlayer.currentMediaItem?.mediaId
val newUri = repository.tryAndRescueError(item, exception = error)
if (newUri != null) {
TODO("Implement handing new uri on fixed error")
@ -155,8 +157,8 @@ class NewPlayerImpl(
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
mutablePlaylist.update {
(0..<internalPlayer.mediaItemCount).map {
internalPlayer.getMediaItemAt(it)
(0..<newExoPlayer.mediaItemCount).map {
newExoPlayer.getMediaItemAt(it)
}
}
}
@ -166,7 +168,12 @@ class NewPlayerImpl(
mutableCurrentlyPlaying.update { mediaItem }
}
})
mutableExoPlayer.update {
newExoPlayer
}
}
init {
playerScope.launch {
currentlyPlaying.collect { playing ->
playing?.let {
@ -183,9 +190,13 @@ class NewPlayerImpl(
}
override fun prepare() {
internalPlayer.prepare()
if(exoPlayer.value == null) {
setupNewExoplayer()
}
exoPlayer.value?.prepare()
if (mediaController == null) {
val sessionToken = SessionToken(app, ComponentName(app, NewPlayerService::class.java))
val sessionToken =
SessionToken(app, ComponentName(app, NewPlayerService::class.java))
val mediaControllerFuture = MediaController.Builder(app, sessionToken).buildAsync()
mediaControllerFuture.addListener({
mediaController = mediaControllerFuture.get()
@ -195,33 +206,38 @@ class NewPlayerImpl(
override fun play() {
if (internalPlayer.currentMediaItem != null) {
internalPlayer.play()
exoPlayer.value?.let {
if (exoPlayer.value?.currentMediaItem != null) {
exoPlayer.value?.play()
} else {
Log.i(TAG, "Tried to start playing but no media Item was cued")
}
}
}
override fun pause() {
internalPlayer.pause()
exoPlayer.value?.pause()
}
override fun addToPlaylist(item: String) {
if (exoPlayer.value == null) {
prepare()
}
launchJobAndCollectError {
val mediaItem = toMediaItem(item)
internalPlayer.addMediaItem(mediaItem)
exoPlayer.value?.addMediaItem(mediaItem)
}
}
override fun movePlaylistItem(fromIndex: Int, toIndex: Int) {
internalPlayer.moveMediaItem(fromIndex, toIndex)
exoPlayer.value?.moveMediaItem(fromIndex, toIndex)
}
override fun removePlaylistItem(uniqueId: Long) {
for (i in 0..<internalPlayer.mediaItemCount) {
val id = internalPlayer.getMediaItemAt(i).mediaId.toLong()
for (i in 0..<(exoPlayer.value?.mediaItemCount ?: 0)) {
val id = exoPlayer.value?.getMediaItemAt(i)?.mediaId?.toLong() ?: 0
if (id == uniqueId) {
internalPlayer.removeMediaItem(i)
exoPlayer.value?.removeMediaItem(i)
break
}
}
@ -234,7 +250,11 @@ class NewPlayerImpl(
}
}
override fun playStream(item: String, streamVariant: String, playMode: PlayMode) {
override fun playStream(
item: String,
streamVariant: String,
playMode: PlayMode
) {
launchJobAndCollectError {
val stream = toMediaItem(item, streamVariant)
internalPlayStream(stream, playMode)
@ -252,22 +272,24 @@ class NewPlayerImpl(
override fun release() {
mediaController?.release()
internalPlayer.release()
exoPlayer.value?.release()
playBackMode.update {
PlayMode.IDLE
}
}
private fun internalPlayStream(mediaItem: MediaItem, playMode: PlayMode) {
if (internalPlayer.playbackState == Player.STATE_IDLE) {
if (exoPlayer.value?.playbackState == Player.STATE_IDLE || exoPlayer.value == null) {
prepare()
}
this.playBackMode.update { playMode }
this.internalPlayer.setMediaItem(mediaItem)
this.internalPlayer.play()
println("gurken: playervalue: ${this.exoPlayer.value}")
this.exoPlayer.value?.setMediaItem(mediaItem)
this.exoPlayer.value?.play()
}
private suspend fun toMediaItem(item: String, streamVariant: String): MediaItem {
private suspend
fun toMediaItem(item: String, streamVariant: String): MediaItem {
val dataStream = repository.getStream(item, streamVariant)
val uniqueId = Random.nextLong()
@ -286,7 +308,8 @@ class NewPlayerImpl(
return mediaItemBuilder.build()
}
private suspend fun toMediaItem(item: String): MediaItem {
private suspend
fun toMediaItem(item: String): MediaItem {
val availableStream = repository.getAvailableStreamVariants(item)
var selectedStream = availableStream[availableStream.size / 2]

View File

@ -138,10 +138,12 @@ class VideoPlayerViewModelImpl @Inject constructor(
private fun installNewPlayer() {
newPlayer?.let { newPlayer ->
val player = newPlayer.internalPlayer
Log.d(TAG, "Install player: ${player.videoSize.width}")
viewModelScope.launch {
newPlayer.exoPlayer.collect { player ->
player.addListener(object : Player.Listener {
Log.d(TAG, "Install player: ${player?.videoSize?.width}")
player?.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
Log.d(TAG, "Playing state changed. Is Playing: $isPlaying")
@ -185,6 +187,9 @@ class VideoPlayerViewModelImpl @Inject constructor(
}
})
}
}
viewModelScope.launch {
newPlayer.playBackMode.collect { newMode ->
val currentMode = mutableUiState.value.uiMode.toPlayMode()
@ -199,6 +204,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
}
}
}
viewModelScope.launch {
newPlayer.playlist.collect { playlist ->
mutableUiState.update {
@ -228,9 +234,9 @@ class VideoPlayerViewModelImpl @Inject constructor(
mutableUiState.update {
it.copy(
playing = newPlayer.internalPlayer.isPlaying,
isLoading = !newPlayer.internalPlayer.isPlaying
&& newPlayer.internalPlayer.isLoading
playing = newPlayer.exoPlayer.value?.isPlaying ?: false,
isLoading = !(newPlayer.exoPlayer.value?.isPlaying
?: false) && newPlayer.exoPlayer.value?.isLoading ?: false
)
}
}
@ -294,7 +300,9 @@ class VideoPlayerViewModelImpl @Inject constructor(
override fun nextStream() {
resetHideUiDelayedJob()
newPlayer?.let { newPlayer ->
if (newPlayer.currentlyPlayingPlaylistItem + 1 < newPlayer.internalPlayer.mediaItemCount) {
if (newPlayer.currentlyPlayingPlaylistItem + 1 <
(newPlayer.exoPlayer.value?.mediaItemCount ?: 0)
) {
newPlayer.currentlyPlayingPlaylistItem += 1
}
}
@ -329,8 +337,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
private fun updateProgressOnce() {
val progress = newPlayer?.currentPosition ?: 0
val duration = newPlayer?.duration ?: 1
val bufferedPercentage =
(newPlayer?.bufferedPercentage?.toFloat() ?: 0f) / 100f
val bufferedPercentage = (newPlayer?.bufferedPercentage?.toFloat() ?: 0f) / 100f
val progressPercentage = progress.toFloat() / duration.toFloat()
mutableUiState.update {
@ -358,8 +365,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
var progress = 0L
val currentlyPlaying = uiState.value.currentlyPlaying?.mediaId?.toLong() ?: 0L
for (item in uiState.value.playList) {
if (item.mediaId.toLong() == currentlyPlaying)
break;
if (item.mediaId.toLong() == currentlyPlaying) break;
progress += item.mediaMetadata.durationMs
?: throw NewPlayerException("Media Item not containing duration. Media Item in question: ${item.mediaMetadata.title}")
}
@ -575,7 +581,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
}
}
private fun getEmbeddedUiRatio() = newPlayer?.internalPlayer?.let { player ->
private fun getEmbeddedUiRatio() = newPlayer?.exoPlayer?.value?.let { player ->
val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio()
return (if (videoRatio.isNaN()) currentContentRatio
else videoRatio).coerceIn(minContentRatio, maxContentRatio)

View File

@ -37,7 +37,6 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.PlayMode
@ -62,7 +61,8 @@ class NewPlayerService : MediaSessionService() {
customCommands = buildCustomCommandList(this)
mediaSession = MediaSession.Builder(this, newPlayer.internalPlayer)
if(newPlayer.exoPlayer.value != null) {
mediaSession = MediaSession.Builder(this, newPlayer.exoPlayer.value!!)
.setCallback(object : MediaSession.Callback {
override fun onConnect(
session: MediaSession,
@ -102,6 +102,7 @@ class NewPlayerService : MediaSessionService() {
CustomCommand.NEW_PLAYER_NOTIFICATION_COMMAND_CLOSE_PLAYBACK -> {
newPlayer.release()
}
else -> {
Log.e(TAG, "Unknown custom command: ${customCommand.customAction}")
return Futures.immediateFuture(SessionResult(SessionError.ERROR_NOT_SUPPORTED))
@ -112,6 +113,9 @@ class NewPlayerService : MediaSessionService() {
})
.build()
} else {
stopSelf()
}
serviceScope.launch {
@ -121,7 +125,6 @@ class NewPlayerService : MediaSessionService() {
}
}
}
}
override fun onDestroy() {
@ -132,7 +135,9 @@ class NewPlayerService : MediaSessionService() {
override fun onTaskRemoved(rootIntent: Intent?) {
// Check if the player is not ready to play or there are no items in the media queue
if (!newPlayer.internalPlayer.playWhenReady || newPlayer.playlist.value.size == 0) {
if (!(newPlayer.exoPlayer.value?.playWhenReady
?: false) || newPlayer.playlist.value.size == 0
) {
// Stop the service
stopSelf()
}

View File

@ -25,6 +25,7 @@ import android.content.pm.ActivityInfo
import android.util.Log
import android.view.SurfaceView
import androidx.activity.compose.BackHandler
import androidx.annotation.OptIn
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
@ -54,7 +55,7 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.media3.common.Player
import net.newpipe.newplayer.model.EmbeddedUiConfig
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.model.UIModeState
import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
@ -66,6 +67,7 @@ import net.newpipe.newplayer.utils.setScreenBrightness
private const val TAG = "VideoPlayerUI"
@OptIn(UnstableApi::class)
@Composable
fun VideoPlayerUI(
viewModel: VideoPlayerViewModel?,
@ -76,6 +78,7 @@ fun VideoPlayerUI(
VideoPlayerLoadingPlaceholder(viewModel.uiState.collectAsState().value.embeddedUiRatio)
} else {
val uiState by viewModel.uiState.collectAsState()
val exoPlayer by viewModel.newPlayer?.exoPlayer!!.collectAsState()
var lifecycle by remember {
mutableStateOf(Lifecycle.Event.ON_CREATE)
@ -174,9 +177,11 @@ fun VideoPlayerUI(
.aspectRatio(uiState.embeddedUiRatio)
), color = Color.Black
) {
exoPlayer?.let { exoPlayer ->
Box(contentAlignment = Alignment.Center) {
PlaySurface(
player = viewModel.newPlayer?.internalPlayer,
player = exoPlayer,
lifecycle = lifecycle,
fitMode = uiState.contentFitMode,
uiRatio = if (uiState.uiMode.fullscreen) screenRatio
@ -184,6 +189,9 @@ fun VideoPlayerUI(
contentRatio = uiState.contentRatio
)
}
}
// the checks if VideoPlayerControllerUI should be visible or not are done by
// The VideoPlayerControllerUI composable itself. This is because Visibility of

View File

@ -28,6 +28,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.launch
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.NewPlayerImpl
import javax.inject.Singleton
@ -38,7 +39,7 @@ object NewPlayerComponent {
@Provides
@Singleton
fun provideNewPlayer(app: Application) : NewPlayer {
val player = NewPlayer.Builder(app, TestMediaRepository(app)).build()
val player = NewPlayerImpl(app, TestMediaRepository(app))
if(app is NewPlayerApp) {
app.appScope.launch {
while(true) {