631 lines
No EOL
22 KiB
Kotlin
631 lines
No EOL
22 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.OptIn
|
|
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 androidx.media3.common.util.UnstableApi
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.SharedFlow
|
|
import kotlinx.coroutines.flow.asSharedFlow
|
|
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.NewPlayerException
|
|
import net.newpipe.newplayer.PlayMode
|
|
import net.newpipe.newplayer.RepeatMode
|
|
import net.newpipe.newplayer.ui.ContentScale
|
|
import java.util.LinkedList
|
|
import kotlin.math.abs
|
|
|
|
val VIDEOPLAYER_UI_STATE = "video_player_ui_state"
|
|
|
|
private const val TAG = "VideoPlayerViewModel"
|
|
|
|
|
|
@UnstableApi
|
|
@HiltViewModel
|
|
class NewPlayerViewModelImpl @Inject constructor(
|
|
private val savedStateHandle: SavedStateHandle,
|
|
application: Application,
|
|
) : AndroidViewModel(application), NewPlayerViewModel {
|
|
|
|
// private
|
|
private val mutableUiState = MutableStateFlow(NewPlayerUIState.DEFAULT)
|
|
private var currentContentRatio = 1F
|
|
|
|
private var playlistItemToBeMoved: Int? = null
|
|
private var playlistItemNewPosition: Int = 0
|
|
|
|
private var hideUiDelayedJob: Job? = null
|
|
private var progressUpdaterJob: Job? = null
|
|
private var playlistProgressUpdaterJob: Job? = null
|
|
|
|
// this is necesary to restore the embedded view UI configuration when returning from fullscreen
|
|
private var embeddedUiConfig: EmbeddedUiConfig? = null
|
|
|
|
private var playbackPositionWhenFastSeekStarted = 0L
|
|
|
|
private val audioManager =
|
|
getSystemService(application.applicationContext, AudioManager::class.java)!!
|
|
|
|
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 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 var mutableEmbeddedPlayerDraggedDownBy = MutableSharedFlow<Float>()
|
|
override val embeddedPlayerDraggedDownBy = mutableEmbeddedPlayerDraggedDownBy.asSharedFlow()
|
|
|
|
private var mutableOnBackPressed = MutableSharedFlow<Unit>()
|
|
override val onBackPressed: SharedFlow<Unit> = mutableOnBackPressed.asSharedFlow()
|
|
|
|
override var deviceInPowerSaveMode: Boolean = false
|
|
get() = field
|
|
set(value) {
|
|
field = value
|
|
if (progressUpdaterJob?.isActive == true) {
|
|
startProgressUpdatePeriodicallyJob()
|
|
}
|
|
}
|
|
|
|
private fun installNewPlayer() {
|
|
newPlayer?.let { newPlayer ->
|
|
viewModelScope.launch {
|
|
newPlayer.exoPlayer.collect { 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, isLoading = false)
|
|
}
|
|
if (isPlaying && uiState.value.uiMode.videoControllerUiVisible) {
|
|
startHideUiDelayedJob()
|
|
} else {
|
|
hideUiDelayedJob?.cancel()
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
newPlayer.playBackMode.collect { newMode ->
|
|
val currentMode = mutableUiState.value.uiMode.toPlayMode()
|
|
|
|
if (currentMode != newMode) {
|
|
changeUiMode(UIModeState.fromPlayMode(newMode), embeddedUiConfig)
|
|
}
|
|
}
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
newPlayer.playlist.collect { playlist ->
|
|
mutableUiState.update {
|
|
it.copy(
|
|
playList = playlist,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
viewModelScope.launch {
|
|
newPlayer.currentlyPlaying.collect { playlistItem ->
|
|
mutableUiState.update {
|
|
it.copy(
|
|
currentlyPlaying = playlistItem,
|
|
currentPlaylistItemIndex = newPlayer.currentlyPlayingPlaylistItem
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
newPlayer.currentChapters.collect { chapters ->
|
|
mutableUiState.update {
|
|
it.copy(chapters = chapters)
|
|
}
|
|
}
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
newPlayer.availableStreamVariants.collect { availableVariants ->
|
|
if (availableVariants != null) {
|
|
mutableUiState.update {
|
|
it.copy(
|
|
availableStreamVariants = availableVariants.identifiers,
|
|
availableLanguages = availableVariants.languages
|
|
)
|
|
}
|
|
} else {
|
|
mutableUiState.update {
|
|
it.copy(
|
|
availableLanguages = emptyList(),
|
|
availableStreamVariants = emptyList()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mutableUiState.update {
|
|
it.copy(
|
|
playing = newPlayer.exoPlayer.value?.isPlaying ?: false,
|
|
isLoading = !(newPlayer.exoPlayer.value?.isPlaying
|
|
?: false) && newPlayer.exoPlayer.value?.isLoading ?: false
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
@OptIn(UnstableApi::class)
|
|
@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, NewPlayerUIState::class.java
|
|
)
|
|
else instanceState.getParcelable(VIDEOPLAYER_UI_STATE)
|
|
|
|
if (recoveredUiState != null) {
|
|
mutableUiState.update {
|
|
recoveredUiState
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun play() {
|
|
changeUiMode(uiState.value.uiMode.getUiHiddenState(), null)
|
|
newPlayer?.play()
|
|
}
|
|
|
|
override fun pause() {
|
|
hideUiDelayedJob?.cancel()
|
|
newPlayer?.pause()
|
|
|
|
}
|
|
|
|
override fun prevStream() {
|
|
startHideUiDelayedJob()
|
|
newPlayer?.let { newPlayer ->
|
|
if (0 <= newPlayer.currentlyPlayingPlaylistItem - 1) {
|
|
newPlayer.currentlyPlayingPlaylistItem -= 1
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun nextStream() {
|
|
startHideUiDelayedJob()
|
|
newPlayer?.let { newPlayer ->
|
|
if (newPlayer.currentlyPlayingPlaylistItem + 1 <
|
|
(newPlayer.exoPlayer.value?.mediaItemCount ?: 0)
|
|
) {
|
|
newPlayer.currentlyPlayingPlaylistItem += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig?) {
|
|
if (newUiModeState == uiState.value.uiMode) {
|
|
return;
|
|
}
|
|
|
|
if (!uiState.value.uiMode.fullscreen && newUiModeState.fullscreen && embeddedUiConfig != null) {
|
|
this.embeddedUiConfig = embeddedUiConfig
|
|
}
|
|
|
|
if (!(newUiModeState == UIModeState.EMBEDDED_VIDEO_CONTROLLER_UI ||
|
|
newUiModeState == UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI)
|
|
) {
|
|
hideUiDelayedJob?.cancel()
|
|
} else {
|
|
startHideUiDelayedJob()
|
|
}
|
|
|
|
if (newUiModeState.isStreamSelect) {
|
|
startPlaylistProgressUpdaterJob()
|
|
} else {
|
|
playlistProgressUpdaterJob?.cancel()
|
|
}
|
|
|
|
if (newUiModeState.requiresProgressUpdate) {
|
|
startProgressUpdatePeriodicallyJob()
|
|
} else {
|
|
progressUpdaterJob?.cancel()
|
|
}
|
|
|
|
if (uiState.value.uiMode.fullscreen && !newUiModeState.fullscreen) {
|
|
mutableUiState.update {
|
|
it.copy(uiMode = newUiModeState, embeddedUiConfig = this.embeddedUiConfig)
|
|
}
|
|
} else {
|
|
mutableUiState.update {
|
|
it.copy(uiMode = newUiModeState)
|
|
}
|
|
}
|
|
|
|
val newPlayMode = newUiModeState.toPlayMode()
|
|
// take the next value from the player because changeUiMode is called when the playBackMode
|
|
// of the player changes. If this value was taken from the viewModel instead
|
|
// this would lead to an endless loop. of changeMode state calling it self over and over again
|
|
// through the callback of the newPlayer?.playBackMode change
|
|
val currentPlayMode = newPlayer?.playBackMode?.value ?: PlayMode.IDLE
|
|
if (newPlayMode != currentPlayMode) {
|
|
newPlayer?.playBackMode?.update {
|
|
newPlayMode
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun startHideUiDelayedJob() {
|
|
hideUiDelayedJob?.cancel()
|
|
hideUiDelayedJob = viewModelScope.launch {
|
|
delay(2000)
|
|
changeUiMode(uiState.value.uiMode.getUiHiddenState(), null)
|
|
}
|
|
}
|
|
|
|
private fun startProgressUpdatePeriodicallyJob() {
|
|
progressUpdaterJob?.cancel()
|
|
progressUpdaterJob = viewModelScope.launch {
|
|
while (true) {
|
|
updateProgressOnce()
|
|
delay(if (deviceInPowerSaveMode) 1000 else 1000 / 30/*fps*/)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun updateProgressOnce() {
|
|
val progress = newPlayer?.currentPosition ?: 0
|
|
val duration = newPlayer?.duration ?: 1
|
|
val bufferedPercentage = (newPlayer?.bufferedPercentage?.toFloat() ?: 0f) / 100f
|
|
val progressPercentage = progress.toFloat() / duration.toFloat()
|
|
|
|
mutableUiState.update {
|
|
it.copy(
|
|
seekerPosition = progressPercentage,
|
|
durationInMs = duration,
|
|
playbackPositionInMs = progress,
|
|
bufferedPercentage = bufferedPercentage,
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun startPlaylistProgressUpdaterJob() {
|
|
playlistProgressUpdaterJob?.cancel()
|
|
playlistProgressUpdaterJob = viewModelScope.launch {
|
|
while (true) {
|
|
updateProgressInPlaylistOnce()
|
|
delay(1000)
|
|
}
|
|
}
|
|
}
|
|
|
|
@OptIn(UnstableApi::class)
|
|
private fun updateProgressInPlaylistOnce() {
|
|
var progress = 0L
|
|
val currentlyPlaying = uiState.value.currentlyPlaying?.mediaId?.toLong() ?: 0L
|
|
for (item in uiState.value.playList) {
|
|
if (item.mediaId.toLong() == currentlyPlaying) break;
|
|
progress += item.mediaMetadata.durationMs
|
|
?: throw NewPlayerException("Media Item not containing duration. Media Item in question: ${item.mediaMetadata.title}")
|
|
}
|
|
progress += (newPlayer?.currentPosition ?: 0)
|
|
mutableUiState.update {
|
|
it.copy(
|
|
playbackPositionInPlaylistMs = progress
|
|
)
|
|
}
|
|
}
|
|
|
|
override fun seekPositionChanged(newValue: Float) {
|
|
hideUiDelayedJob?.cancel()
|
|
progressUpdaterJob?.cancel()
|
|
val seekerPosition = mutableUiState.value.seekerPosition
|
|
val seekPositionInMs = (newPlayer?.duration?.toFloat() ?: 0F) * seekerPosition
|
|
newPlayer?.currentPosition = seekPositionInMs.toLong()
|
|
Log.i(TAG, "Seek to Ms: $seekPositionInMs")
|
|
mutableUiState.update {
|
|
it.copy(
|
|
seekerPosition = newValue,
|
|
playbackPositionInMs = seekPositionInMs.toLong()
|
|
)
|
|
}
|
|
}
|
|
|
|
override fun seekingFinished() {
|
|
startHideUiDelayedJob()
|
|
startProgressUpdatePeriodicallyJob()
|
|
}
|
|
|
|
override fun embeddedDraggedDown(offset: Float) {
|
|
safeTryEmit(mutableEmbeddedPlayerDraggedDownBy, offset)
|
|
}
|
|
|
|
override fun fastSeek(count: Int) {
|
|
if(abs(count) == 1) {
|
|
playbackPositionWhenFastSeekStarted = newPlayer?.currentPosition ?: 0
|
|
}
|
|
|
|
val fastSeekAmountInS = count * (newPlayer?.fastSeekAmountSec ?: 10)
|
|
mutableUiState.update {
|
|
it.copy(
|
|
fastSeekSeconds = fastSeekAmountInS
|
|
)
|
|
}
|
|
|
|
|
|
if (fastSeekAmountInS != 0) {
|
|
Log.d(TAG, "fast seeking seeking by $fastSeekAmountInS seconds")
|
|
|
|
newPlayer?.currentPosition =
|
|
playbackPositionWhenFastSeekStarted + (fastSeekAmountInS * 1000)
|
|
|
|
}
|
|
|
|
if (mutableUiState.value.uiMode.videoControllerUiVisible) {
|
|
startHideUiDelayedJob()
|
|
}
|
|
}
|
|
|
|
override fun finishFastSeek() {
|
|
if (mutableUiState.value.uiMode.videoControllerUiVisible) {
|
|
startHideUiDelayedJob()
|
|
}
|
|
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 onBackPressed() {
|
|
val nextMode = uiState.value.uiMode.getNextModeWhenBackPressed()
|
|
if (nextMode != null) {
|
|
changeUiMode(nextMode, null)
|
|
} else {
|
|
safeTryEmit(mutableOnBackPressed, Unit)
|
|
}
|
|
}
|
|
|
|
override fun chapterSelected(chapterId: Int) {
|
|
newPlayer?.selectChapter(chapterId)
|
|
}
|
|
|
|
override fun streamSelected(streamId: Int) {
|
|
newPlayer?.currentlyPlayingPlaylistItem = streamId
|
|
}
|
|
|
|
override fun cycleRepeatMode() {
|
|
newPlayer?.let {
|
|
it.repeatMode = when (it.repeatMode) {
|
|
RepeatMode.DO_NOT_REPEAT -> RepeatMode.REPEAT_ALL
|
|
RepeatMode.REPEAT_ALL -> RepeatMode.REPEAT_ONE
|
|
RepeatMode.REPEAT_ONE -> RepeatMode.DO_NOT_REPEAT
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun toggleShuffle() {
|
|
newPlayer?.let {
|
|
it.shuffle = !it.shuffle
|
|
}
|
|
}
|
|
|
|
override fun onStorePlaylist() {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
override fun movePlaylistItem(from: Int, to: Int) {
|
|
if (playlistItemToBeMoved == null) {
|
|
playlistItemToBeMoved = from
|
|
}
|
|
playlistItemNewPosition = to
|
|
val tempList = LinkedList(uiState.value.playList)
|
|
val item = uiState.value.playList[from]
|
|
tempList.removeAt(from)
|
|
tempList.add(to, item)
|
|
mutableUiState.update {
|
|
it.copy(
|
|
playList = tempList
|
|
)
|
|
}
|
|
startPlaylistProgressUpdaterJob()
|
|
}
|
|
|
|
override fun onStreamItemDragFinished() {
|
|
playlistItemToBeMoved?.let {
|
|
newPlayer?.movePlaylistItem(it, playlistItemNewPosition)
|
|
}
|
|
playlistItemToBeMoved = null
|
|
}
|
|
|
|
override fun dialogVisible(visible: Boolean) {
|
|
if (visible) {
|
|
hideUiDelayedJob?.cancel()
|
|
} else {
|
|
startHideUiDelayedJob()
|
|
}
|
|
}
|
|
|
|
override fun removePlaylistItem(uniqueId: Long) {
|
|
newPlayer?.removePlaylistItem(uniqueId)
|
|
}
|
|
|
|
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)
|
|
|
|
|
|
} ?: minContentRatio
|
|
|
|
private fun <T> safeTryEmit(sharedFlow: MutableSharedFlow<T>, value: T) {
|
|
if (!sharedFlow.tryEmit(value)) {
|
|
viewModelScope.launch {
|
|
sharedFlow.emit(value)
|
|
}
|
|
}
|
|
}
|
|
} |