make duration update work

This commit is contained in:
Christian Schabesberger 2024-07-29 15:54:00 +02:00
parent fdd55bf4a1
commit a64faae788
8 changed files with 474 additions and 377 deletions

View File

@ -0,0 +1,36 @@
package net.newpipe.newplayer.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import net.newpipe.newplayer.ui.ContentScale
@Parcelize
data class VideoPlayerUIState(
val playing: Boolean,
var fullscreen: Boolean,
val uiVissible: Boolean,
var uiVisible: Boolean,
val contentRatio: Float,
val embeddedUiRatio: Float,
val contentFitMode: ContentScale,
val seekerPosition: Float,
val isLoading: Boolean,
val durationInMs: Long,
val playbackPositionInMs: Long
) : Parcelable {
companion object {
val DEFAULT = VideoPlayerUIState(
playing = false,
fullscreen = false,
uiVissible = false,
uiVisible = false,
contentRatio = 16 / 9F,
embeddedUiRatio = 16F / 9F,
contentFitMode = ContentScale.FIT_INSIDE,
seekerPosition = 0F,
isLoading = true,
durationInMs = 0,
playbackPositionInMs = 0
)
}
}

View File

@ -1,79 +1,11 @@
/* 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 package net.newpipe.newplayer.model
import android.app.Application
import android.os.Build
import android.os.Bundle 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.lifecycle.viewModelScope
import androidx.media3.common.Player 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.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.newpipe.newplayer.utils.VideoSize
import kotlinx.parcelize.Parcelize
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
val VIDEOPLAYER_UI_STATE = "video_player_ui_state"
private const val TAG = "VideoPlayerViewModel"
@Parcelize
data class VideoPlayerUIState(
val playing: Boolean,
var fullscreen: Boolean,
val uiVissible: Boolean,
var uiVisible: Boolean,
val contentRatio: Float,
val embeddedUiRatio: Float,
val contentFitMode: ContentScale,
val seekerPosition: Float,
val isLoading: Boolean
) : Parcelable {
companion object {
val DEFAULT = VideoPlayerUIState(
playing = false,
fullscreen = false,
uiVissible = false,
uiVisible = false,
contentRatio = 16 / 9F,
embeddedUiRatio = 16F / 9F,
contentFitMode = ContentScale.FIT_INSIDE,
seekerPosition = 0F,
isLoading = true
)
}
}
interface VideoPlayerViewModel { interface VideoPlayerViewModel {
var newPlayer: NewPlayer? var newPlayer: NewPlayer?
@ -101,300 +33,3 @@ interface VideoPlayerViewModel {
fun onUiVissibleToggle(isVissible: Boolean) fun onUiVissibleToggle(isVissible: Boolean)
} }
} }
@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
//interface
override var callbackListener: VideoPlayerViewModel.Listener? = null
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(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 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))
}
override fun onIsLoadingChanged(isLoading: Boolean) {
super.onIsLoadingChanged(isLoading)
mutableUiState.update {
it.copy(isLoading = isLoading)
}
Log.i(TAG, if (isLoading) "Player started loading" else "Player finished loading")
}
})
}
}
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 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() {
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() {
if (mutableUiState.value.fullscreen)
callbackListener?.onUiVissibleToggle(true)
mutableUiState.update {
it.copy(uiVissible = true)
}
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 = player?.currentPosition ?: 0
val duration = player?.duration ?: 1
val progressPercentage = progress.toFloat() / duration.toFloat()
mutableUiState.update {
it.copy(seekerPosition = progressPercentage)
}
}
override fun hideUi() {
if (mutableUiState.value.fullscreen)
callbackListener?.onUiVissibleToggle(false)
progressUpdaterJob?.cancel()
uiVisibilityJob?.cancel()
mutableUiState.update {
it.copy(uiVissible = false)
}
}
override fun seekPositionChanged(newValue: Float) {
uiVisibilityJob?.cancel()
mutableUiState.update { it.copy(seekerPosition = newValue) }
}
override fun seekingFinished() {
resetHideUiDelayedJob()
val seekerPosition = mutableUiState.value.seekerPosition
val seekPositionInMs = (player?.duration?.toFloat() ?: 0F) * seekerPosition
player?.seekTo(seekPositionInMs.toLong())
Log.i(TAG, "Seek to Ms: $seekPositionInMs")
}
override fun switchToEmbeddedView() {
callbackListener?.onFullscreenToggle(false)
uiVisibilityJob?.cancel()
mutableUiState.update {
it.copy(fullscreen = false, uiVissible = false)
}
}
override fun switchToFullscreen() {
callbackListener?.onFullscreenToggle(true)
uiVisibilityJob?.cancel()
mutableUiState.update {
it.copy(fullscreen = true, uiVissible = false)
}
}
private fun getEmbeddedUiRatio() =
player?.let { player ->
val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio()
return (if (videoRatio.isNaN())
currentContentRatio
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 var contentFitMode = ContentScale.FIT_INSIDE
override var callbackListener: VideoPlayerViewModel.Listener? = null
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 showUi() {
println("dummy impl")
}
override fun hideUi() {
println("dummy impl")
}
override fun seekPositionChanged(newValue: Float) {
println("dummy impl")
}
override fun seekingFinished() {
println("dummy impl")
}
override fun pause() {
println("dummy pause")
}
override fun prevStream() {
println("dummy impl")
}
override fun nextStream() {
println("dummy impl")
}
}
}
}

View File

@ -0,0 +1,350 @@
/* 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.util.Log
import androidx.annotation.RequiresApi
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.StateFlow
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
//interface
override var callbackListener: VideoPlayerViewModel.Listener? = null
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(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 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))
}
override fun onIsLoadingChanged(isLoading: Boolean) {
super.onIsLoadingChanged(isLoading)
mutableUiState.update {
it.copy(isLoading = isLoading)
}
Log.i(
TAG,
if (isLoading) "Player started loading" else "Player finished loading"
)
}
})
}
}
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 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() {
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() {
if (mutableUiState.value.fullscreen)
callbackListener?.onUiVissibleToggle(true)
mutableUiState.update {
it.copy(uiVissible = true)
}
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 = player?.currentPosition ?: 0
val duration = player?.duration ?: 1
val progressPercentage = progress.toFloat() / duration.toFloat()
mutableUiState.update {
it.copy(
seekerPosition = progressPercentage,
durationInMs = duration,
playbackPositionInMs = progress
)
}
}
override fun hideUi() {
if (mutableUiState.value.fullscreen)
callbackListener?.onUiVissibleToggle(false)
progressUpdaterJob?.cancel()
uiVisibilityJob?.cancel()
mutableUiState.update {
it.copy(uiVissible = false)
}
}
override fun seekPositionChanged(newValue: Float) {
uiVisibilityJob?.cancel()
mutableUiState.update { it.copy(seekerPosition = newValue) }
}
override fun seekingFinished() {
resetHideUiDelayedJob()
val seekerPosition = mutableUiState.value.seekerPosition
val seekPositionInMs = (player?.duration?.toFloat() ?: 0F) * seekerPosition
player?.seekTo(seekPositionInMs.toLong())
Log.i(TAG, "Seek to Ms: $seekPositionInMs")
}
override fun switchToEmbeddedView() {
callbackListener?.onFullscreenToggle(false)
uiVisibilityJob?.cancel()
mutableUiState.update {
it.copy(fullscreen = false, uiVissible = false)
}
}
override fun switchToFullscreen() {
callbackListener?.onFullscreenToggle(true)
uiVisibilityJob?.cancel()
mutableUiState.update {
it.copy(fullscreen = true, uiVissible = false)
}
}
private fun getEmbeddedUiRatio() =
player?.let { player ->
val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio()
return (if (videoRatio.isNaN())
currentContentRatio
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 var contentFitMode = ContentScale.FIT_INSIDE
override var callbackListener: VideoPlayerViewModel.Listener? = null
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 showUi() {
println("dummy impl")
}
override fun hideUi() {
println("dummy impl")
}
override fun seekPositionChanged(newValue: Float) {
println("dummy impl")
}
override fun seekingFinished() {
println("dummy impl")
}
override fun pause() {
println("dummy pause")
}
override fun prevStream() {
println("dummy impl")
}
override fun nextStream() {
println("dummy impl")
}
}
}
}

View File

@ -59,6 +59,8 @@ fun VideoPlayerControllerUI(
uiVissible: Boolean, uiVissible: Boolean,
seekPosition: Float, seekPosition: Float,
isLoading: Boolean, isLoading: Boolean,
durationInMs: Long,
playbackPositionInMs: Long,
play: () -> Unit, play: () -> Unit,
pause: () -> Unit, pause: () -> Unit,
prevStream: () -> Unit, prevStream: () -> Unit,
@ -153,11 +155,13 @@ fun VideoPlayerControllerUI(
.defaultMinSize(minHeight = 40.dp) .defaultMinSize(minHeight = 40.dp)
.fillMaxWidth(), .fillMaxWidth(),
isFullscreen = fullscreen, isFullscreen = fullscreen,
seekPosition, durationInMs = durationInMs,
switchToFullscreen, playbackPositionInMs = playbackPositionInMs,
switchToEmbeddedView, seekPosition = seekPosition,
seekPositionChanged, switchToFullscreen = switchToFullscreen,
seekingFinished switchToEmbeddedView = switchToEmbeddedView,
seekPositionChanged = seekPositionChanged,
seekingFinished = seekingFinished
) )
} }
} }
@ -197,6 +201,8 @@ fun VideoPlayerControllerUIPreviewEmbedded() {
uiVissible = true, uiVissible = true,
seekPosition = 0.3F, seekPosition = 0.3F,
isLoading = false, isLoading = false,
durationInMs = 9*60*1000,
playbackPositionInMs = 6*60*1000,
play = {}, play = {},
pause = {}, pause = {},
prevStream = {}, prevStream = {},
@ -221,6 +227,8 @@ fun VideoPlayerControllerUIPreviewLandscape() {
uiVissible = true, uiVissible = true,
seekPosition = 0.3F, seekPosition = 0.3F,
isLoading = true, isLoading = true,
durationInMs = 9*60*1000,
playbackPositionInMs = 6*60*1000,
play = {}, play = {},
pause = {}, pause = {},
prevStream = {}, prevStream = {},
@ -246,6 +254,8 @@ fun VideoPlayerControllerUIPreviewPortrait() {
uiVissible = true, uiVissible = true,
seekPosition = 0.3F, seekPosition = 0.3F,
isLoading = false, isLoading = false,
durationInMs = 9*60*1000,
playbackPositionInMs = 6*60*1000,
play = {}, play = {},
pause = {}, pause = {},
prevStream = {}, prevStream = {},

View File

@ -119,6 +119,8 @@ fun VideoPlayerUI(
uiVissible = uiState.uiVissible, uiVissible = uiState.uiVissible,
seekPosition = uiState.seekerPosition, seekPosition = uiState.seekerPosition,
isLoading = uiState.isLoading, isLoading = uiState.isLoading,
durationInMs = uiState.durationInMs,
playbackPositionInMs = uiState.playbackPositionInMs,
play = viewModel::play, play = viewModel::play,
pause = viewModel::pause, pause = viewModel::pause,
prevStream = viewModel::prevStream, prevStream = viewModel::prevStream,

View File

@ -70,7 +70,7 @@ val video_player_scrim = Color(0xFF000000)
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape") @Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
@Composable @Composable
fun VideoPlayerControllerUIPreviewEmbeddedColorpreview() { fun VideoPlayerControllerUIPreviewEmbeddedColorPreview() {
VideoPlayerTheme { VideoPlayerTheme {
PreviewBackgroundSurface { PreviewBackgroundSurface {
VideoPlayerControllerUI(isPlaying = false, VideoPlayerControllerUI(isPlaying = false,
@ -78,6 +78,8 @@ fun VideoPlayerControllerUIPreviewEmbeddedColorpreview() {
uiVissible = true, uiVissible = true,
seekPosition = 0.3F, seekPosition = 0.3F,
isLoading = false, isLoading = false,
durationInMs = 9*60*1000,
playbackPositionInMs = 6*60*1000,
play = {}, play = {},
pause = {}, pause = {},
prevStream = {}, prevStream = {},

View File

@ -20,6 +20,8 @@
package net.newpipe.newplayer.ui.videoplayer package net.newpipe.newplayer.ui.videoplayer
import android.app.LocaleConfig
import android.icu.text.DecimalFormat
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -30,32 +32,38 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.core.os.ConfigurationCompat
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.ui.seeker.Seeker import net.newpipe.newplayer.ui.seeker.Seeker
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import java.util.Locale
import kotlin.math.min
@Composable @Composable
fun BottomUI( fun BottomUI(
modifier: Modifier, modifier: Modifier,
isFullscreen: Boolean, isFullscreen: Boolean,
seekPosition: Float, seekPosition: Float,
durationInMs: Long,
playbackPositionInMs: Long,
switchToFullscreen: () -> Unit, switchToFullscreen: () -> Unit,
switchToEmbeddedView: () -> Unit, switchToEmbeddedView: () -> Unit,
seekPositionChanged: (Float) -> Unit, seekPositionChanged: (Float) -> Unit,
seekingFinished: () -> Unit seekingFinished: () -> Unit
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier modifier = modifier
) { ) {
Text("00:06:45") Text(getTimeStringFromMs(playbackPositionInMs, getLocale() ?: Locale.US))
Seeker( Seeker(
Modifier.weight(1F), Modifier.weight(1F),
value = seekPosition, value = seekPosition,
@ -65,7 +73,7 @@ fun BottomUI(
//Slider(value = 0.4F, onValueChange = {}, modifier = Modifier.weight(1F)) //Slider(value = 0.4F, onValueChange = {}, modifier = Modifier.weight(1F))
Text("00:09:40") Text(getTimeStringFromMs(durationInMs, getLocale() ?: Locale.US))
IconButton(onClick = if (isFullscreen) switchToEmbeddedView else switchToFullscreen) { IconButton(onClick = if (isFullscreen) switchToEmbeddedView else switchToFullscreen) {
Icon( Icon(
@ -77,6 +85,42 @@ fun BottomUI(
} }
} }
@Composable
@ReadOnlyComposable
fun getLocale(): Locale? {
val configuration = LocalConfiguration.current
return ConfigurationCompat.getLocales(configuration).get(0)
}
private const val HOURS_PER_DAY = 24
private const val MINUTES_PER_HOUR = 60
private const val SECONDS_PER_MINUTE = 60
private const val MILLIS_PER_SECOND = 1000
private const val MILLIS_PER_DAY =
HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLIS_PER_SECOND
private const val MILLIS_PER_HOUR = MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLIS_PER_SECOND
private const val MILLIS_PER_MINUTE = SECONDS_PER_MINUTE * MILLIS_PER_SECOND
private fun getTimeStringFromMs(timeSpanInMs: Long, locale: Locale) : String {
val days = timeSpanInMs / MILLIS_PER_DAY
val millisThisDay = timeSpanInMs - days * MILLIS_PER_DAY
val hours = millisThisDay / MILLIS_PER_HOUR
val millisThisHour = millisThisDay - hours * MILLIS_PER_HOUR
val minutes = millisThisHour / MILLIS_PER_MINUTE
val milliesThisMinute = millisThisHour - minutes * MILLIS_PER_MINUTE
val seconds = milliesThisMinute / MILLIS_PER_SECOND
val time_string =
if (0L < days) String.format(locale, "%d:%02d:%02d:%02d", days, hours, minutes, seconds)
else if (0L < hours) String.format(locale, "%d:%02d:%02d", hours, minutes, seconds)
else String.format(locale, "%d:%02d", minutes, seconds)
return time_string
}
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
// Preview // Preview
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
@ -90,6 +134,8 @@ fun VideoPlayerControllerBottomUIPreview() {
modifier = Modifier, modifier = Modifier,
isFullscreen = true, isFullscreen = true,
seekPosition = 0.4F, seekPosition = 0.4F,
durationInMs = 90 * 60 * 1000,
playbackPositionInMs = 3 * 60 * 1000,
switchToFullscreen = { }, switchToFullscreen = { },
switchToEmbeddedView = { }, switchToEmbeddedView = { },
seekPositionChanged = {} seekPositionChanged = {}

View File

@ -33,6 +33,7 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -41,12 +42,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
@Composable @Composable
fun DropDownMenu() { fun DropDownMenu() {
@ -123,4 +127,16 @@ fun DropDownMenu() {
} }
} }
}
///////////////////////////////////////////////////////////////////
// Preview
///////////////////////////////////////////////////////////////////
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
@Composable
fun VideoPlayerControllerDropDownPreview() {
VideoPlayerTheme {
DropDownMenu()
}
} }