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

379 lines
No EOL
12 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.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.media3.common.Player
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.newpipe.newplayer.utils.VideoSize
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.ui.ContentScale
val VIDEOPLAYER_UI_STATE = "video_player_ui_state"
private const val TAG = "VideoPlayerViewModel"
@HiltViewModel
class VideoPlayerViewModelImpl @Inject constructor(
private val savedStateHandle: SavedStateHandle,
application: Application,
) : AndroidViewModel(application), VideoPlayerViewModel {
// private
private val mutableUiState = MutableStateFlow(VideoPlayerUIState.DEFAULT)
private var currentContentRatio = 1F
private var uiVisibilityJob: Job? = null
private var progressUpdaterJob: Job? = null
private val audioManager =
getSystemService(application.applicationContext, AudioManager::class.java)!!
var callbackListeners: MutableList<VideoPlayerViewModel.Listener?> = ArrayList()
init {
val soundVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() /
audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC).toFloat()
mutableUiState.update {
it.copy(soundVolume = soundVolume)
}
}
//interface
override var newPlayer: NewPlayer? = null
set(value) {
field = value
installNewPlayer()
}
override val uiState = mutableUiState.asStateFlow()
override val internalPlayer: Player?
get() = newPlayer?.internalPlayer
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(embeddedUiRatio = getEmbeddedUiRatio()) }
}
}
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(embeddedUiRatio = getEmbeddedUiRatio()) }
}
}
override var contentFitMode: ContentScale
get() = mutableUiState.value.contentFitMode
set(value) {
mutableUiState.update {
it.copy(contentFitMode = value)
}
}
private fun installNewPlayer() {
internalPlayer?.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))
}
// TODO: This is not correctly applicable for loading indicator
override fun onIsLoadingChanged(isLoading: Boolean) {
super.onIsLoadingChanged(isLoading)
mutableUiState.update {
it.copy(isLoading = isLoading)
}
}
})
}
newPlayer?.let { newPlayer ->
viewModelScope.launch {
newPlayer.playMode.collect { newMode ->
val currentMode = mutableUiState.value.uiMode.toPlayMode()
if (currentMode != newMode) {
mutableUiState.update {
it.copy(uiMode = UIModeState.fromPlayMode(newMode))
}
}
}
}
}
}
fun updateContentRatio(videoSize: VideoSize) {
val newRatio = videoSize.getRatio()
val ratio = if (newRatio.isNaN()) currentContentRatio else newRatio
currentContentRatio = ratio
Log.d(TAG, "Update Content ratio: $ratio")
mutableUiState.update {
it.copy(
contentRatio = currentContentRatio, embeddedUiRatio = getEmbeddedUiRatio()
)
}
}
override fun onCleared() {
super.onCleared()
Log.d(TAG, "viewmodel cleared")
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun initUIState(instanceState: Bundle) {
val recoveredUiState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) instanceState.getParcelable(
VIDEOPLAYER_UI_STATE, VideoPlayerUIState::class.java
)
else instanceState.getParcelable(VIDEOPLAYER_UI_STATE)
if(recoveredUiState != null) {
mutableUiState.update {
recoveredUiState
}
}
}
override fun addCallbackListener(listener: VideoPlayerViewModel.Listener) {
callbackListeners.add(listener)
}
override fun play() {
hideUi()
newPlayer?.play()
}
override fun pause() {
uiVisibilityJob?.cancel()
newPlayer?.pause()
}
override fun prevStream() {
resetHideUiDelayedJob()
Log.e(TAG, "imeplement prev stream")
}
override fun nextStream() {
resetHideUiDelayedJob()
Log.e(TAG, "implement next stream")
}
override fun showUi() {
mutableUiState.update {
it.copy(uiMode = it.uiMode.getControllerUiVisibleState())
}
resetHideUiDelayedJob()
resetProgressUpdatePeriodicallyJob()
}
private fun resetHideUiDelayedJob() {
uiVisibilityJob?.cancel()
uiVisibilityJob = viewModelScope.launch {
delay(4000)
hideUi()
}
}
private fun resetProgressUpdatePeriodicallyJob() {
progressUpdaterJob?.cancel()
progressUpdaterJob = viewModelScope.launch {
while (true) {
updateProgressOnce()
delay(1000)
}
}
}
private fun updateProgressOnce() {
val progress = internalPlayer?.currentPosition ?: 0
val duration = internalPlayer?.duration ?: 1
val bufferedPercentage = (internalPlayer?.bufferedPercentage?.toFloat() ?: 0f) / 100f
val progressPercentage = progress.toFloat() / duration.toFloat()
mutableUiState.update {
it.copy(
seekerPosition = progressPercentage,
durationInMs = duration,
playbackPositionInMs = progress,
bufferedPercentage = bufferedPercentage
)
}
}
override fun hideUi() {
progressUpdaterJob?.cancel()
uiVisibilityJob?.cancel()
mutableUiState.update {
it.copy(uiMode = it.uiMode.getUiHiddenState())
}
}
override fun seekPositionChanged(newValue: Float) {
uiVisibilityJob?.cancel()
mutableUiState.update { it.copy(seekerPosition = newValue) }
}
override fun seekingFinished() {
resetHideUiDelayedJob()
val seekerPosition = mutableUiState.value.seekerPosition
val seekPositionInMs = (internalPlayer?.duration?.toFloat() ?: 0F) * seekerPosition
newPlayer?.currentPosition = seekPositionInMs.toLong()
Log.i(TAG, "Seek to Ms: $seekPositionInMs")
}
override fun embeddedDraggedDown(offset: Float) {
callbackListeners.forEach { it?.embeddedPlayerDraggedDown(offset) }
}
override fun fastSeek(count: Int) {
mutableUiState.update {
it.copy(
fastSeekSeconds = count * (newPlayer?.fastSeekAmountSec ?: 10)
)
}
if (mutableUiState.value.uiMode.controllerUiVisible) {
resetHideUiDelayedJob()
}
}
override fun finishFastSeek() {
val fastSeekAmount = mutableUiState.value.fastSeekSeconds
if (fastSeekAmount != 0) {
Log.d(TAG, "$fastSeekAmount")
newPlayer?.currentPosition = (newPlayer?.currentPosition ?: 0) + (fastSeekAmount * 1000)
mutableUiState.update {
it.copy(fastSeekSeconds = 0)
}
}
}
override fun brightnessChange(changeRate: Float, systemBrightness: Float) {
if (mutableUiState.value.uiMode.fullscreen) {
val currentBrightness = mutableUiState.value.brightness
?: if (systemBrightness < 0f) 0.5f else systemBrightness
Log.d(
TAG,
"currentBrightnes: $currentBrightness, sytemBrightness: $systemBrightness, changeRate: $changeRate"
)
val newBrightness = (currentBrightness + changeRate * 1.3f).coerceIn(0f, 1f)
mutableUiState.update {
it.copy(brightness = newBrightness)
}
}
}
override fun volumeChange(changeRate: Float) {
val currentVolume = mutableUiState.value.soundVolume
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC).toFloat()
// we multiply changeRate by 1.5 so your finger only has to swipe a portion of the whole
// screen in order to fully enable or disable the volume
val newVolume = (currentVolume + changeRate * 1.3f).coerceIn(0f, 1f)
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC, (newVolume * maxVolume).toInt(), 0
)
println("Blub: currentVolume: $currentVolume, changeRate: $changeRate, maxVolume: $maxVolume, newvolume: $newVolume")
mutableUiState.update {
it.copy(soundVolume = newVolume)
}
}
override fun switchToEmbeddedView() {
callbackListeners.forEach { it?.onFullscreenToggle(false) }
uiVisibilityJob?.cancel()
finishFastSeek()
updateUiMode(UIModeState.EMBEDDED_VIDEO)
}
override fun switchToFullscreen() {
callbackListeners.forEach { it?.onFullscreenToggle(true) }
uiVisibilityJob?.cancel()
finishFastSeek()
updateUiMode(UIModeState.FULLSCREEN_VIDEO)
}
private fun updateUiMode(newState: UIModeState) {
val newPlayMode = newState.toPlayMode()
val currentPlayMode = mutableUiState.value.uiMode.toPlayMode()
if(newPlayMode != currentPlayMode) {
newPlayer?.setPlayMode(newPlayMode!!)
} else {
mutableUiState.update {
it.copy(uiMode = newState)
}
}
}
private fun getEmbeddedUiRatio() = internalPlayer?.let { player ->
val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio()
return (if (videoRatio.isNaN()) currentContentRatio
else videoRatio).coerceIn(minContentRatio, maxContentRatio)
} ?: minContentRatio
}