From ac165143c51a7a00ee50ceb357923ace39515027 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Tue, 15 Oct 2024 18:03:14 +0200 Subject: [PATCH] show available video streams --- .../net/newpipe/newplayer/NewPlayerImpl.kt | 5 +- .../newpipe/newplayer/data/StreamSelection.kt | 36 +++++++++ .../newpipe/newplayer/logic/StreamSelector.kt | 18 ++--- .../net/newpipe/newplayer/logic/TrackUtils.kt | 28 +++++-- .../ui/videoplayer/controller/Menu.kt | 4 +- .../ui/videoplayer/controller/TopUI.kt | 76 +++++++++++++++---- new-player/src/main/res/values/strings.xml | 3 +- 7 files changed, 137 insertions(+), 33 deletions(-) diff --git a/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt b/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt index 139fc853..3146d7b1 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt @@ -63,6 +63,7 @@ import net.newpipe.newplayer.data.StreamSelection import net.newpipe.newplayer.logic.StreamSelectionResponse import net.newpipe.newplayer.data.StreamTrack import net.newpipe.newplayer.logic.StreamSelector +import net.newpipe.newplayer.logic.TrackUtils import kotlin.random.Random private const val TAG = "NewPlayerImpl" @@ -207,7 +208,7 @@ class NewPlayerImpl( uniqueIdToStreamSelectionLookup[mediaItem.mediaId.toLong()]!! launchJobAndCollectError { mutableCurrentlyAvailableTracks.update { - StreamSelector.getNonDynamicTracksNonDuplicated(repository.getStreams(streamSelection.item)) + TrackUtils.getNonDynamicTracksNonDuplicated(repository.getStreams(streamSelection.item)) } } } else { @@ -344,7 +345,7 @@ class NewPlayerImpl( override fun playStream(item: String, playMode: PlayMode) { launchJobAndCollectError { mutableCurrentlyAvailableTracks.update { - StreamSelector.getNonDynamicTracksNonDuplicated(repository.getStreams(item)) + TrackUtils.getNonDynamicTracksNonDuplicated(repository.getStreams(item)) } val mediaSource = toMediaSource(item) diff --git a/new-player/src/main/java/net/newpipe/newplayer/data/StreamSelection.kt b/new-player/src/main/java/net/newpipe/newplayer/data/StreamSelection.kt index 41845e4b..38bdc1f8 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/data/StreamSelection.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/data/StreamSelection.kt @@ -20,8 +20,15 @@ package net.newpipe.newplayer.data +import net.newpipe.newplayer.logic.TrackUtils + interface StreamSelection { val item: String + + val tracks : List + val hasVideoTracks:Boolean + val hasAudioTracks:Boolean + val isDynamic:Boolean } data class SingleSelection( @@ -29,11 +36,40 @@ data class SingleSelection( ) : StreamSelection { override val item: String get() = stream.item + + override val tracks: List + get() = stream.streamTracks + + override val hasVideoTracks: Boolean + get() = stream.hasVideoTracks + + override val hasAudioTracks: Boolean + get() = stream.hasVideoTracks + + override val isDynamic: Boolean + get() = stream.isDashOrHls } data class MultiSelection( val streams: List ) : StreamSelection { + override val item: String get() = streams[0].item + + override val tracks: List + get() { + val allTracks = mutableListOf() + streams.forEach { allTracks.addAll(it.streamTracks) } + return allTracks + } + + override val hasVideoTracks: Boolean + get() = TrackUtils.hasVideoTracks(streams) + + override val hasAudioTracks: Boolean + get() = TrackUtils.hasAudioTracks(streams) + + override val isDynamic: Boolean + get() = TrackUtils.hasDynamicStreams(streams) } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/logic/StreamSelector.kt b/new-player/src/main/java/net/newpipe/newplayer/logic/StreamSelector.kt index 82033be2..c44562fd 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/logic/StreamSelector.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/logic/StreamSelector.kt @@ -27,10 +27,10 @@ import net.newpipe.newplayer.data.SingleSelection import net.newpipe.newplayer.data.Stream import net.newpipe.newplayer.data.StreamSelection import net.newpipe.newplayer.logic.TrackUtils.getDynamicStreams -import net.newpipe.newplayer.logic.TrackUtils.hasVideoStreams -import net.newpipe.newplayer.logic.TrackUtils.tryAndGetMedianAudioOnlyStream -import net.newpipe.newplayer.logic.TrackUtils.tryAndGetMedianCombinedVideoAndAudioStream -import net.newpipe.newplayer.logic.TrackUtils.tryAndGetMedianVideoOnlyStream +import net.newpipe.newplayer.logic.TrackUtils.hasVideoTracks +import net.newpipe.newplayer.logic.TrackUtils.tryAndGetMedianAudioOnlyTracks +import net.newpipe.newplayer.logic.TrackUtils.tryAndGetMedianCombinedVideoAndAudioTracks +import net.newpipe.newplayer.logic.TrackUtils.tryAndGetMedianVideoOnlyTracks internal class StreamSelector( val preferredLanguages: List, @@ -55,7 +55,7 @@ internal class StreamSelector( // is it a video stream or a pure audio stream? - if (hasVideoStreams(availableStreams)) { + if (hasVideoTracks(availableStreams)) { // first: try and get a dynamic stream variant val dynamicStreams = getDynamicStreams(availablesInPreferredLanguage) @@ -65,12 +65,12 @@ internal class StreamSelector( // second: try and get separate audio and video stream variants - val videoOnlyStream = tryAndGetMedianVideoOnlyStream(availableStreams) + val videoOnlyStream = tryAndGetMedianVideoOnlyTracks(availableStreams) if (videoOnlyStream != null) { - val audioStream = tryAndGetMedianAudioOnlyStream(availableStreams) + val audioStream = tryAndGetMedianAudioOnlyTracks(availableStreams) if (videoOnlyStream != null && audioStream != null) { return MultiSelection(listOf(videoOnlyStream, audioStream)) @@ -79,7 +79,7 @@ internal class StreamSelector( // fourth: try to get a video and audio stream variant with the best fitting identifier - tryAndGetMedianCombinedVideoAndAudioStream(availableStreams)?.let { + tryAndGetMedianCombinedVideoAndAudioTracks(availableStreams)?.let { return SingleSelection(it) } @@ -87,7 +87,7 @@ internal class StreamSelector( // first: try to get an audio stream variant with the best fitting identifier - tryAndGetMedianAudioOnlyStream(availableStreams)?.let { + tryAndGetMedianAudioOnlyTracks(availableStreams)?.let { return SingleSelection(it) } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/logic/TrackUtils.kt b/new-player/src/main/java/net/newpipe/newplayer/logic/TrackUtils.kt index f2eb3d52..80ef61cb 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/logic/TrackUtils.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/logic/TrackUtils.kt @@ -66,20 +66,20 @@ object TrackUtils { ) } - internal fun tryAndGetMedianVideoOnlyStream(availableStreams: List) = + internal fun tryAndGetMedianVideoOnlyTracks(availableStreams: List) = availableStreams.filter { !it.isDashOrHls && it.hasVideoTracks && !it.hasAudioTracks } .ifEmpty { null }?.let { it[it.size / 2] } - internal fun tryAndGetMedianCombinedVideoAndAudioStream(availableStreams: List) = + internal fun tryAndGetMedianCombinedVideoAndAudioTracks(availableStreams: List) = availableStreams.filter { !it.isDashOrHls && it.hasVideoTracks && it.hasVideoTracks } .ifEmpty { null } ?.let { it[it.size / 2] } - internal fun tryAndGetMedianAudioOnlyStream(availableStreams: List) = + internal fun tryAndGetMedianAudioOnlyTracks(availableStreams: List) = availableStreams.filter { !it.isDashOrHls && it.hasAudioTracks && !it.hasVideoTracks } .ifEmpty { null }?.let { it[it.size / 2] @@ -96,19 +96,35 @@ object TrackUtils { internal fun getDynamicStreams(availableStreams: List) = availableStreams.filter { it.isDashOrHls } - internal fun getNonDynamicVideoStreams(availableStreams: List) = + internal fun getNonDynamicVideoTracks(availableStreams: List) = availableStreams.filter { !it.isDashOrHls && it.hasVideoTracks && !it.hasAudioTracks } - internal fun getNonDynamicAudioStreams(availableStreams: List) = + internal fun getNonDynamicAudioTracks(availableStreams: List) = availableStreams.filter { !it.isDashOrHls && !it.hasVideoTracks && it.hasAudioTracks } - internal fun hasVideoStreams(availableStreams: List): Boolean { + internal fun hasVideoTracks(availableStreams: List): Boolean { availableStreams.forEach { if (it.hasVideoTracks) return true } return false } + + internal fun hasAudioTracks(availableStreams: List): Boolean { + availableStreams.forEach { + if (it.hasAudioTracks) + return true + } + return false + } + + internal fun hasDynamicStreams(availableStreams: List): Boolean { + availableStreams.forEach { + if (it.isDashOrHls) + return true + } + return false + } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/Menu.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/Menu.kt index a1d8e424..3355e173 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/Menu.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/Menu.kt @@ -65,7 +65,7 @@ import net.newpipe.newplayer.ui.common.getEmbeddedUiConfig @OptIn(UnstableApi::class) @Composable -internal fun DropDownMenu(viewModel: InternalNewPlayerViewModel, uiState: NewPlayerUIState) { +internal fun VideoPlayerMenu(viewModel: InternalNewPlayerViewModel, uiState: NewPlayerUIState) { var showMainMenu: Boolean by remember { mutableStateOf(false) } val pixel_density = LocalDensity.current @@ -184,7 +184,7 @@ internal fun DropDownMenu(viewModel: InternalNewPlayerViewModel, uiState: NewPla private fun VideoPlayerControllerDropDownPreview() { VideoPlayerTheme { Box(Modifier.fillMaxSize()) { - DropDownMenu(NewPlayerViewModelDummy(), NewPlayerUIState.DUMMY) + VideoPlayerMenu(NewPlayerViewModelDummy(), NewPlayerUIState.DUMMY) } } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/TopUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/TopUI.kt index 12d5c772..6cc47962 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/TopUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/controller/TopUI.kt @@ -21,6 +21,7 @@ package net.newpipe.newplayer.ui.videoplayer.controller import android.app.Activity +import android.widget.Toast import androidx.annotation.OptIn import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement @@ -32,13 +33,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.automirrored.filled.MenuBook +import androidx.compose.material.icons.filled.Language import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -52,6 +60,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.media3.common.util.UnstableApi import net.newpipe.newplayer.R +import net.newpipe.newplayer.data.VideoStreamTrack +import net.newpipe.newplayer.logic.TrackUtils import net.newpipe.newplayer.uiModel.EmbeddedUiConfig import net.newpipe.newplayer.uiModel.NewPlayerUIState import net.newpipe.newplayer.uiModel.InternalNewPlayerViewModel @@ -86,7 +96,7 @@ internal fun TopUI( maxLines = 1, overflow = TextOverflow.Ellipsis ) - + val creatorOffset = with(LocalDensity.current) { 14.sp.toDp() } @@ -101,17 +111,7 @@ internal fun TopUI( } else { Box(modifier = Modifier.weight(1F)) } - Button( - onClick = { /*TODO*/ }, - contentPadding = PaddingValues(0.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Transparent, contentColor = video_player_onSurface - ), - ) { - Text( - "1080p", fontWeight = FontWeight.Bold, modifier = Modifier.padding(0.dp) - ) - } + TrackSelectionMenu(viewModel, uiState) IconButton( onClick = { /*TODO*/ }, ) { @@ -149,7 +149,57 @@ internal fun TopUI( ) } } - DropDownMenu(viewModel, uiState) + VideoPlayerMenu(viewModel, uiState) + } +} + +@OptIn(UnstableApi::class) +@Composable +private fun TrackSelectionMenu(viewModel: InternalNewPlayerViewModel, uiState: NewPlayerUIState) { + var menuVisible by remember { + mutableStateOf(false) + } + + val context = LocalContext.current + val noOtherTracksText = stringResource( + id = R.string.no_other_tracks_available_toast + ) + + val availableVideoTracks = + uiState.currentlyAvailableTracks.filterIsInstance() + + Box { + Button( + onClick = { + if (1 < availableVideoTracks.size) { + viewModel.dialogVisible(true) + menuVisible = true + } else + Toast.makeText( + context, + noOtherTracksText, + Toast.LENGTH_SHORT + + ).show() + }, + contentPadding = PaddingValues(0.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, contentColor = video_player_onSurface + ), + ) { + Text( + "1080p", fontWeight = FontWeight.Bold, modifier = Modifier.padding(0.dp) + ) + } + DropdownMenu(expanded = menuVisible, onDismissRequest = { menuVisible = false }) { + for (track in availableVideoTracks) { + DropdownMenuItem(text = { Text(track.toLongIdentifierString()) }, + onClick = { /*TODO*/ + viewModel.dialogVisible(false) + menuVisible = false + }) + } + } } } diff --git a/new-player/src/main/res/values/strings.xml b/new-player/src/main/res/values/strings.xml index 8f5b1d1f..bc8fb8d9 100644 --- a/new-player/src/main/res/values/strings.xml +++ b/new-player/src/main/res/values/strings.xml @@ -61,5 +61,6 @@ Switch to fullscreen video mode Picture in picture Playing in the background… - Video seek preview thumbnail + Video seek preview thumbnaila + No other stream tracks available. \ No newline at end of file