From 4fb5d46b2d96e58efbbf7cdfb134ba5ab17fb198 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Wed, 11 Sep 2024 16:23:14 +0200 Subject: [PATCH] make exoPlayer a StateFlow that can be null --- .../java/net/newpipe/newplayer/NewPlayer.kt | 34 +---- .../net/newpipe/newplayer/NewPlayerImpl.kt | 117 +++++++++++------- .../model/VideoPlayerViewModelImpl.kt | 104 ++++++++-------- .../newplayer/service/NewPlayerService.kt | 101 ++++++++------- .../net/newpipe/newplayer/ui/VideoPlayerUI.kt | 28 +++-- .../newplayer/testapp/NewPlayerComponent.kt | 3 +- 6 files changed, 199 insertions(+), 188 deletions(-) diff --git a/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt b/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt index 7efb4c5..9c26e46 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt @@ -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 - val internalPlayer: Player + val exoPlayer: StateFlow 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 = emptyList() - - fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory): Builder { - this.mediaSourceFactory = mediaSourceFactory - return this - } - - fun setPreferredStreamVariants(preferredStreamVariants: List): 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, - ) - } - } - } diff --git a/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt b/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt index dbe8f06..52f2dc2 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt @@ -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, - private val repository: MediaRepository + private val repository: MediaRepository, + override val preferredStreamVariants: List = emptyList() ) : NewPlayer { + private val mutableExoPlayer = MutableStateFlow(null) + override val exoPlayer = mutableExoPlayer.asStateFlow() + private var playerScope = CoroutineScope(Dispatchers.Main + Job()) private var uniqueIdToIdLookup = HashMap() @@ -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>(emptyList()) override val playlist: StateFlow> = @@ -124,19 +125,20 @@ class NewPlayerImpl( override val currentChapters: StateFlow> = mutableCurrentChapter.asStateFlow() override var currentlyPlayingPlaylistItem: Int - get() = internalPlayer.currentMediaItemIndex + get() = exoPlayer.value?.currentMediaItemIndex ?: -1 set(value) { assert(value in 0.. 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,36 +206,41 @@ class NewPlayerImpl( override fun play() { - if (internalPlayer.currentMediaItem != null) { - internalPlayer.play() - } else { - Log.i(TAG, "Tried to start playing but no media Item was cued") + 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.. - val player = newPlayer.internalPlayer - Log.d(TAG, "Install player: ${player.videoSize.width}") + viewModelScope.launch { + newPlayer.exoPlayer.collect { player -> - player.addListener(object : Player.Listener { - override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - Log.d(TAG, "Playing state changed. Is Playing: $isPlaying") - mutableUiState.update { - it.copy(playing = isPlaying, isLoading = false) - } - if (isPlaying && uiState.value.uiMode.controllerUiVisible) { - resetHideUiDelayedJob() - } else { - uiVisibilityJob?.cancel() - } - } + Log.d(TAG, "Install player: ${player?.videoSize?.width}") - override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) { - super.onVideoSizeChanged(videoSize) - updateContentRatio(VideoSize.fromMedia3VideoSize(videoSize)) - } - - - override fun onIsLoadingChanged(isLoading: Boolean) { - super.onIsLoadingChanged(isLoading) - if (!player.isPlaying) { - mutableUiState.update { - it.copy(isLoading = isLoading) + player?.addListener(object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + Log.d(TAG, "Playing state changed. Is Playing: $isPlaying") + mutableUiState.update { + it.copy(playing = isPlaying, isLoading = false) + } + if (isPlaying && uiState.value.uiMode.controllerUiVisible) { + resetHideUiDelayedJob() + } else { + uiVisibilityJob?.cancel() + } } - } - } - override fun onRepeatModeChanged(repeatMode: Int) { - super.onRepeatModeChanged(repeatMode) - mutableUiState.update { - it.copy(repeatMode = newPlayer.repeatMode) - } - } + override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) { + super.onVideoSizeChanged(videoSize) + updateContentRatio(VideoSize.fromMedia3VideoSize(videoSize)) + } + + + override fun onIsLoadingChanged(isLoading: Boolean) { + super.onIsLoadingChanged(isLoading) + if (!player.isPlaying) { + mutableUiState.update { + it.copy(isLoading = isLoading) + } + } + } + + override fun onRepeatModeChanged(repeatMode: Int) { + super.onRepeatModeChanged(repeatMode) + mutableUiState.update { + it.copy(repeatMode = newPlayer.repeatMode) + } + } + + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + super.onShuffleModeEnabledChanged(shuffleModeEnabled) + mutableUiState.update { + it.copy(shuffleEnabled = newPlayer.shuffle) + } + } + }) - override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { - super.onShuffleModeEnabledChanged(shuffleModeEnabled) - mutableUiState.update { - it.copy(shuffleEnabled = newPlayer.shuffle) - } } - }) + } viewModelScope.launch { newPlayer.playBackMode.collect { newMode -> @@ -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) diff --git a/new-player/src/main/java/net/newpipe/newplayer/service/NewPlayerService.kt b/new-player/src/main/java/net/newpipe/newplayer/service/NewPlayerService.kt index 5d676bd..1da4800 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/service/NewPlayerService.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/service/NewPlayerService.kt @@ -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,66 +61,70 @@ class NewPlayerService : MediaSessionService() { customCommands = buildCustomCommandList(this) - mediaSession = MediaSession.Builder(this, newPlayer.internalPlayer) - .setCallback(object : MediaSession.Callback { - override fun onConnect( - session: MediaSession, - controller: MediaSession.ControllerInfo - ): MediaSession.ConnectionResult { - val connectionResult = super.onConnect(session, controller) - val availableSessionCommands = - connectionResult.availableSessionCommands.buildUpon() + if(newPlayer.exoPlayer.value != null) { + mediaSession = MediaSession.Builder(this, newPlayer.exoPlayer.value!!) + .setCallback(object : MediaSession.Callback { + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ): MediaSession.ConnectionResult { + val connectionResult = super.onConnect(session, controller) + val availableSessionCommands = + connectionResult.availableSessionCommands.buildUpon() - customCommands.forEach { command -> - command.commandButton.sessionCommand?.let { - availableSessionCommands.add(it) + customCommands.forEach { command -> + command.commandButton.sessionCommand?.let { + availableSessionCommands.add(it) + } } + + return MediaSession.ConnectionResult.accept( + availableSessionCommands.build(), + connectionResult.availablePlayerCommands + ) } - return MediaSession.ConnectionResult.accept( - availableSessionCommands.build(), - connectionResult.availablePlayerCommands - ) - } - - override fun onPostConnect( - session: MediaSession, - controller: MediaSession.ControllerInfo - ) { - super.onPostConnect(session, controller) - mediaSession.setCustomLayout(customCommands.map{it.commandButton}) - } - - override fun onCustomCommand( - session: MediaSession, - controller: MediaSession.ControllerInfo, - customCommand: SessionCommand, - args: Bundle - ): ListenableFuture { - when(customCommand.customAction) { - 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)) - } + override fun onPostConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ) { + super.onPostConnect(session, controller) + mediaSession.setCustomLayout(customCommands.map { it.commandButton }) } - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } - }) - .build() + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + when (customCommand.customAction) { + 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)) + } + } + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + + }) + .build() + } else { + stopSelf() + } serviceScope.launch { newPlayer.playBackMode.collect { mode -> - if(mode == PlayMode.IDLE) { + if (mode == PlayMode.IDLE) { stopSelf() } } } - } 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() } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt index db543b5..50fcdb7 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt @@ -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,17 +177,22 @@ fun VideoPlayerUI( .aspectRatio(uiState.embeddedUiRatio) ), color = Color.Black ) { - Box(contentAlignment = Alignment.Center) { - PlaySurface( - player = viewModel.newPlayer?.internalPlayer, - lifecycle = lifecycle, - fitMode = uiState.contentFitMode, - uiRatio = if (uiState.uiMode.fullscreen) screenRatio - else uiState.embeddedUiRatio, - contentRatio = uiState.contentRatio - ) + + exoPlayer?.let { exoPlayer -> + Box(contentAlignment = Alignment.Center) { + PlaySurface( + player = exoPlayer, + lifecycle = lifecycle, + fitMode = uiState.contentFitMode, + uiRatio = if (uiState.uiMode.fullscreen) screenRatio + else uiState.embeddedUiRatio, + contentRatio = uiState.contentRatio + ) + } } + + // the checks if VideoPlayerControllerUI should be visible or not are done by // The VideoPlayerControllerUI composable itself. This is because Visibility of // the controller is more complicated than just using a simple if statement. diff --git a/test-app/src/main/java/net/newpipe/newplayer/testapp/NewPlayerComponent.kt b/test-app/src/main/java/net/newpipe/newplayer/testapp/NewPlayerComponent.kt index c33aaee..ab18fbb 100644 --- a/test-app/src/main/java/net/newpipe/newplayer/testapp/NewPlayerComponent.kt +++ b/test-app/src/main/java/net/newpipe/newplayer/testapp/NewPlayerComponent.kt @@ -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) {