NewPlayer/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt

260 lines
No EOL
7.6 KiB
Kotlin

/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/
package net.newpipe.newplayer.model
import android.app.Application
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.media3.common.Player
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import net.newpipe.newplayer.utils.VideoSize
import kotlinx.parcelize.Parcelize
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.ui.ContentFitMode
val VIDEOPLAYER_UI_STATE = "video_player_ui_state"
private const val TAG = "VideoPlayerViewModel"
@Parcelize
data class VideoPlayerUIState(
val playing: Boolean,
var fullscreen: Boolean,
var uiVisible: Boolean,
val contentRatio: Float,
val uiRatio: Float,
val contentFitMode: ContentFitMode
) : Parcelable {
companion object {
val DEFAULT = VideoPlayerUIState(
playing = false,
fullscreen = false,
uiVisible = false,
contentRatio = 16 / 9F,
uiRatio = 16F / 9F,
contentFitMode = ContentFitMode.FIT_INSIDE
)
}
}
interface VideoPlayerViewModel {
var newPlayer: NewPlayer?
val player: Player?
val uiState: StateFlow<VideoPlayerUIState>
var minContentRatio: Float
var maxContentRatio: Float
fun initUIState(instanceState: Bundle)
fun play()
fun pause()
fun prevStream()
fun nextStream()
fun switchToFullscreen()
fun switchToEmbeddedView()
}
@HiltViewModel
class VideoPlayerViewModelImpl @Inject constructor(
private val savedStateHandle: SavedStateHandle,
application: Application,
) : AndroidViewModel(application), VideoPlayerViewModel {
// private
private val mutableUiState = MutableStateFlow(VideoPlayerUIState.DEFAULT)
//interface
override var newPlayer: NewPlayer? = null
set(value) {
field = value
installExoPlayer()
}
override val uiState = mutableUiState.asStateFlow()
override val player: Player?
get() = newPlayer?.player
override var minContentRatio: Float = 4F / 3F
set(value) {
if (value <= 0 || maxContentRatio < value)
Log.e(
TAG,
"Ignoring maxContentRatio: It must not be 0 or less and it may not be bigger then mmaxContentRatio. It was Set to: $value"
)
else {
field = value
mutableUiState.update { it.copy(uiRatio = getUiRatio()) }
}
}
override var maxContentRatio: Float = 16F / 9F
set(value) {
if (value <= 0 || value < minContentRatio)
Log.e(
TAG,
"Ignoring maxContentRatio: It must not be 0 or less and it may not be smaller then minContentRatio. It was Set to: $value"
)
else {
field = value
mutableUiState.update { it.copy(uiRatio = getUiRatio()) }
}
}
private fun installExoPlayer() {
player?.let { player ->
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")
mutableUiState.update {
it.copy(playing = isPlaying)
}
}
override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) {
super.onVideoSizeChanged(videoSize)
updateContentRatio(VideoSize.fromMedia3VideoSize(videoSize))
}
})
}
}
fun updateContentRatio(videoSize: VideoSize) {
val newRatio = videoSize.getRatio()
Log.d(TAG, "Update Content ratio: $newRatio")
mutableUiState.update {
it.copy(
contentRatio = newRatio,
uiRatio = getUiRatio()
)
}
}
override fun onCleared() {
super.onCleared()
Log.d(TAG, "viewmodel cleared")
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun initUIState(instanceState: Bundle) {
val uiState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) instanceState.getParcelable(
VIDEOPLAYER_UI_STATE, VideoPlayerUIState::class.java
)
else instanceState.getParcelable(VIDEOPLAYER_UI_STATE)
uiState?.let { uiState ->
mutableUiState.update {
uiState
}
}
}
override fun play() {
newPlayer?.play()
}
override fun pause() {
newPlayer?.pause()
}
override fun prevStream() {
Log.e(TAG, "imeplement prev stream")
}
override fun nextStream() {
Log.e(TAG, "implement next stream")
}
override fun switchToEmbeddedView() {
mutableUiState.update {
it.copy(fullscreen = false)
}
}
override fun switchToFullscreen() {
mutableUiState.update {
it.copy(fullscreen = true)
}
}
private fun getUiRatio() =
player?.let { player ->
val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio()
return if (videoRatio.isNaN())
minContentRatio
else
videoRatio.coerceIn(minContentRatio, maxContentRatio)
} ?: minContentRatio
companion object {
val dummy = object : VideoPlayerViewModel {
override var newPlayer: NewPlayer? = null
override val player: Player? = null
override val uiState = MutableStateFlow(VideoPlayerUIState.DEFAULT)
override var minContentRatio = 4F / 3F
override var maxContentRatio = 16F / 9F
override fun initUIState(instanceState: Bundle) {
println("dummy impl")
}
override fun play() {
println("dummy impl")
}
override fun switchToEmbeddedView() {
println("dummy impl")
}
override fun switchToFullscreen() {
println("dummy impl")
}
override fun pause() {
println("dummy pause")
}
override fun prevStream() {
println("dummy impl")
}
override fun nextStream() {
println("dummy impl")
}
}
}
}