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

View File

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

View File

@ -37,7 +37,6 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.PlayMode import net.newpipe.newplayer.PlayMode
@ -62,66 +61,70 @@ class NewPlayerService : MediaSessionService() {
customCommands = buildCustomCommandList(this) customCommands = buildCustomCommandList(this)
mediaSession = MediaSession.Builder(this, newPlayer.internalPlayer) if(newPlayer.exoPlayer.value != null) {
.setCallback(object : MediaSession.Callback { mediaSession = MediaSession.Builder(this, newPlayer.exoPlayer.value!!)
override fun onConnect( .setCallback(object : MediaSession.Callback {
session: MediaSession, override fun onConnect(
controller: MediaSession.ControllerInfo session: MediaSession,
): MediaSession.ConnectionResult { controller: MediaSession.ControllerInfo
val connectionResult = super.onConnect(session, controller) ): MediaSession.ConnectionResult {
val availableSessionCommands = val connectionResult = super.onConnect(session, controller)
connectionResult.availableSessionCommands.buildUpon() val availableSessionCommands =
connectionResult.availableSessionCommands.buildUpon()
customCommands.forEach { command -> customCommands.forEach { command ->
command.commandButton.sessionCommand?.let { command.commandButton.sessionCommand?.let {
availableSessionCommands.add(it) availableSessionCommands.add(it)
}
} }
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
connectionResult.availablePlayerCommands
)
} }
return MediaSession.ConnectionResult.accept( override fun onPostConnect(
availableSessionCommands.build(), session: MediaSession,
connectionResult.availablePlayerCommands controller: MediaSession.ControllerInfo
) ) {
} super.onPostConnect(session, controller)
mediaSession.setCustomLayout(customCommands.map { it.commandButton })
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<SessionResult> {
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))
}
}) override fun onCustomCommand(
.build() session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
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 { serviceScope.launch {
newPlayer.playBackMode.collect { mode -> newPlayer.playBackMode.collect { mode ->
if(mode == PlayMode.IDLE) { if (mode == PlayMode.IDLE) {
stopSelf() stopSelf()
} }
} }
} }
} }
override fun onDestroy() { override fun onDestroy() {
@ -132,7 +135,9 @@ class NewPlayerService : MediaSessionService() {
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
// Check if the player is not ready to play or there are no items in the media queue // 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 // Stop the service
stopSelf() stopSelf()
} }

View File

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

View File

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