Compare commits

...

10 Commits

25 changed files with 826 additions and 391 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
.kotlin

View File

@ -46,6 +46,7 @@ coil = "2.7.0"
reorderable = "2.4.0-alpha02" reorderable = "2.4.0-alpha02"
media3Session = "1.4.1" media3Session = "1.4.1"
media3ExoplayerDash = "1.4.1" media3ExoplayerDash = "1.4.1"
adaptiveAndroid = "1.0.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -83,6 +84,7 @@ coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coi
reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" } reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" }
androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3Session" } androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3Session" }
androidx-media3-exoplayer-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "media3ExoplayerDash" } androidx-media3-exoplayer-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "media3ExoplayerDash" }
androidx-adaptive-android = { group = "androidx.compose.material3.adaptive", name = "adaptive-android", version.ref = "adaptiveAndroid" }

View File

@ -70,6 +70,7 @@ dependencies {
implementation(libs.reorderable) implementation(libs.reorderable)
implementation(libs.androidx.media3.session) implementation(libs.androidx.media3.session)
implementation(libs.androidx.media3.exoplayer.dash) implementation(libs.androidx.media3.exoplayer.dash)
implementation(libs.androidx.adaptive.android)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler) ksp(libs.androidx.hilt.compiler)

View File

@ -4,7 +4,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<application> <application
android:resizeableActivity="true">
<service <service
android:name=".service.NewPlayerService" android:name=".service.NewPlayerService"
android:foregroundServiceType="mediaPlayback" android:foregroundServiceType="mediaPlayback"

View File

@ -38,16 +38,15 @@ interface NewPlayerViewModel {
var contentFitMode: ContentScale var contentFitMode: ContentScale
val embeddedPlayerDraggedDownBy: SharedFlow<Float> val embeddedPlayerDraggedDownBy: SharedFlow<Float>
val onBackPressed: SharedFlow<Unit> val onBackPressed: SharedFlow<Unit>
var deviceInPowerSaveMode: Boolean
fun initUIState(instanceState: Bundle) fun initUIState(instanceState: Bundle)
fun play() fun play()
fun pause() fun pause()
fun prevStream() fun prevStream()
fun nextStream() fun nextStream()
fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig) fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig?)
fun onBackPressed() fun onBackPressed()
fun showUi()
fun hideUi()
fun seekPositionChanged(newValue: Float) fun seekPositionChanged(newValue: Float)
fun seekingFinished() fun seekingFinished()
fun embeddedDraggedDown(offset: Float) fun embeddedDraggedDown(offset: Float)

View File

@ -18,6 +18,7 @@ open class NewPlayerViewModelDummy : NewPlayerViewModel {
override var contentFitMode = ContentScale.FIT_INSIDE override var contentFitMode = ContentScale.FIT_INSIDE
override val embeddedPlayerDraggedDownBy = MutableSharedFlow<Float>().asSharedFlow() override val embeddedPlayerDraggedDownBy = MutableSharedFlow<Float>().asSharedFlow()
override val onBackPressed: SharedFlow<Unit> = MutableSharedFlow<Unit>().asSharedFlow() override val onBackPressed: SharedFlow<Unit> = MutableSharedFlow<Unit>().asSharedFlow()
override var deviceInPowerSaveMode: Boolean = false
override fun initUIState(instanceState: Bundle) { override fun initUIState(instanceState: Bundle) {
println("dummy impl") println("dummy impl")
@ -31,14 +32,6 @@ open class NewPlayerViewModelDummy : NewPlayerViewModel {
println("dummy impl") println("dummy impl")
} }
override fun showUi() {
println("dummy impl")
}
override fun hideUi() {
println("dummy impl")
}
override fun seekPositionChanged(newValue: Float) { override fun seekPositionChanged(newValue: Float) {
println("dymmy seekPositionChanged: newValue: ${newValue}") println("dymmy seekPositionChanged: newValue: ${newValue}")
} }
@ -115,7 +108,7 @@ open class NewPlayerViewModelDummy : NewPlayerViewModel {
println("dummy impl") println("dummy impl")
} }
override fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig) { override fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig?) {
println("dummy uiMode change: New UI Mode State: $newUiModeState") println("dummy uiMode change: New UI Mode State: $newUiModeState")
} }
} }

View File

@ -47,9 +47,11 @@ import kotlinx.coroutines.launch
import net.newpipe.newplayer.utils.VideoSize import net.newpipe.newplayer.utils.VideoSize
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.NewPlayerException import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.PlayMode
import net.newpipe.newplayer.RepeatMode import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
import java.util.LinkedList import java.util.LinkedList
import kotlin.math.abs
val VIDEOPLAYER_UI_STATE = "video_player_ui_state" val VIDEOPLAYER_UI_STATE = "video_player_ui_state"
@ -70,13 +72,15 @@ class NewPlayerViewModelImpl @Inject constructor(
private var playlistItemToBeMoved: Int? = null private var playlistItemToBeMoved: Int? = null
private var playlistItemNewPosition: Int = 0 private var playlistItemNewPosition: Int = 0
private var uiVisibilityJob: Job? = null private var hideUiDelayedJob: Job? = null
private var progressUpdaterJob: Job? = null private var progressUpdaterJob: Job? = null
private var playlistProgressUpdaterJob: Job? = null private var playlistProgressUpdaterJob: Job? = null
// this is necesary to restore the embedded view UI configuration when returning from fullscreen // this is necesary to restore the embedded view UI configuration when returning from fullscreen
private var embeddedUiConfig: EmbeddedUiConfig? = null private var embeddedUiConfig: EmbeddedUiConfig? = null
private var playbackPositionWhenFastSeekStarted = 0L
private val audioManager = private val audioManager =
getSystemService(application.applicationContext, AudioManager::class.java)!! getSystemService(application.applicationContext, AudioManager::class.java)!!
@ -136,6 +140,15 @@ class NewPlayerViewModelImpl @Inject constructor(
private var mutableOnBackPressed = MutableSharedFlow<Unit>() private var mutableOnBackPressed = MutableSharedFlow<Unit>()
override val onBackPressed: SharedFlow<Unit> = mutableOnBackPressed.asSharedFlow() 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() { private fun installNewPlayer() {
newPlayer?.let { newPlayer -> newPlayer?.let { newPlayer ->
viewModelScope.launch { viewModelScope.launch {
@ -151,9 +164,9 @@ class NewPlayerViewModelImpl @Inject constructor(
it.copy(playing = isPlaying, isLoading = false) it.copy(playing = isPlaying, isLoading = false)
} }
if (isPlaying && uiState.value.uiMode.videoControllerUiVisible) { if (isPlaying && uiState.value.uiMode.videoControllerUiVisible) {
resetHideUiDelayedJob() startHideUiDelayedJob()
} else { } else {
uiVisibilityJob?.cancel() hideUiDelayedJob?.cancel()
} }
} }
@ -195,12 +208,7 @@ class NewPlayerViewModelImpl @Inject constructor(
val currentMode = mutableUiState.value.uiMode.toPlayMode() val currentMode = mutableUiState.value.uiMode.toPlayMode()
if (currentMode != newMode) { if (currentMode != newMode) {
mutableUiState.update { changeUiMode(UIModeState.fromPlayMode(newMode), embeddedUiConfig)
it.copy(
uiMode = UIModeState.fromPlayMode(newMode),
embeddedUiConfig = embeddedUiConfig
)
}
} }
} }
} }
@ -299,18 +307,18 @@ class NewPlayerViewModelImpl @Inject constructor(
} }
override fun play() { override fun play() {
hideUi() changeUiMode(uiState.value.uiMode.getUiHiddenState(), null)
newPlayer?.play() newPlayer?.play()
} }
override fun pause() { override fun pause() {
uiVisibilityJob?.cancel() hideUiDelayedJob?.cancel()
newPlayer?.pause() newPlayer?.pause()
} }
override fun prevStream() { override fun prevStream() {
resetHideUiDelayedJob() startHideUiDelayedJob()
newPlayer?.let { newPlayer -> newPlayer?.let { newPlayer ->
if (0 <= newPlayer.currentlyPlayingPlaylistItem - 1) { if (0 <= newPlayer.currentlyPlayingPlaylistItem - 1) {
newPlayer.currentlyPlayingPlaylistItem -= 1 newPlayer.currentlyPlayingPlaylistItem -= 1
@ -319,7 +327,7 @@ class NewPlayerViewModelImpl @Inject constructor(
} }
override fun nextStream() { override fun nextStream() {
resetHideUiDelayedJob() startHideUiDelayedJob()
newPlayer?.let { newPlayer -> newPlayer?.let { newPlayer ->
if (newPlayer.currentlyPlayingPlaylistItem + 1 < if (newPlayer.currentlyPlayingPlaylistItem + 1 <
(newPlayer.exoPlayer.value?.mediaItemCount ?: 0) (newPlayer.exoPlayer.value?.mediaItemCount ?: 0)
@ -329,65 +337,72 @@ class NewPlayerViewModelImpl @Inject constructor(
} }
} }
override fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig) { override fun changeUiMode(newUiModeState: UIModeState, embeddedUiConfig: EmbeddedUiConfig?) {
if (!uiState.value.uiMode.fullscreen && newUiModeState.fullscreen) { if (newUiModeState == uiState.value.uiMode) {
return;
}
if (!uiState.value.uiMode.fullscreen && newUiModeState.fullscreen && embeddedUiConfig != null) {
this.embeddedUiConfig = embeddedUiConfig this.embeddedUiConfig = embeddedUiConfig
} }
if(!(newUiModeState == UIModeState.EMBEDDED_VIDEO_CONTROLLER_UI || if (!(newUiModeState == UIModeState.EMBEDDED_VIDEO_CONTROLLER_UI ||
newUiModeState == UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI)) { newUiModeState == UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI)
uiVisibilityJob?.cancel() ) {
hideUiDelayedJob?.cancel()
} else { } else {
resetHideUiDelayedJob() startHideUiDelayedJob()
} }
if (newUiModeState.isStreamSelect) { if (newUiModeState.isStreamSelect) {
resetPlaylistProgressUpdaterJob() startPlaylistProgressUpdaterJob()
} } else {
if (newUiModeState.isChapterSelect) {
resetPlaylistProgressUpdaterJob()
}
if ((uiState.value.uiMode.isStreamSelect || uiState.value.uiMode.isChapterSelect)
&& (!newUiModeState.isStreamSelect && !newUiModeState.isChapterSelect)
) {
playlistProgressUpdaterJob?.cancel() playlistProgressUpdaterJob?.cancel()
}
if (newUiModeState.requiresProgressUpdate) {
startProgressUpdatePeriodicallyJob()
} else {
progressUpdaterJob?.cancel() progressUpdaterJob?.cancel()
} }
updateUiMode(newUiModeState) 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
}
}
} }
override fun showUi() { private fun startHideUiDelayedJob() {
mutableUiState.update { hideUiDelayedJob?.cancel()
it.copy(uiMode = it.uiMode.getControllerUiVisibleState()) hideUiDelayedJob = viewModelScope.launch {
}
resetHideUiDelayedJob()
resetProgressUpdatePeriodicallyJob()
}
private fun resetHideUiDelayedJob() {
var ex:Exception? = null
try {
throw Exception()
} catch(e: Exception) {
ex = e
}
uiVisibilityJob?.cancel()
uiVisibilityJob = viewModelScope.launch {
delay(2000) delay(2000)
hideUi() changeUiMode(uiState.value.uiMode.getUiHiddenState(), null)
ex?.printStackTrace()
} }
} }
private fun resetProgressUpdatePeriodicallyJob() { private fun startProgressUpdatePeriodicallyJob() {
progressUpdaterJob?.cancel() progressUpdaterJob?.cancel()
progressUpdaterJob = viewModelScope.launch { progressUpdaterJob = viewModelScope.launch {
while (true) { while (true) {
updateProgressOnce() updateProgressOnce()
delay(1000) delay(if (deviceInPowerSaveMode) 1000 else 1000 / 30/*fps*/)
} }
} }
} }
@ -408,7 +423,7 @@ class NewPlayerViewModelImpl @Inject constructor(
} }
} }
private fun resetPlaylistProgressUpdaterJob() { private fun startPlaylistProgressUpdaterJob() {
playlistProgressUpdaterJob?.cancel() playlistProgressUpdaterJob?.cancel()
playlistProgressUpdaterJob = viewModelScope.launch { playlistProgressUpdaterJob = viewModelScope.launch {
while (true) { while (true) {
@ -435,25 +450,24 @@ class NewPlayerViewModelImpl @Inject constructor(
} }
} }
override fun hideUi() {
progressUpdaterJob?.cancel()
uiVisibilityJob?.cancel()
mutableUiState.update {
it.copy(uiMode = it.uiMode.getUiHiddenState())
}
}
override fun seekPositionChanged(newValue: Float) { override fun seekPositionChanged(newValue: Float) {
uiVisibilityJob?.cancel() hideUiDelayedJob?.cancel()
mutableUiState.update { it.copy(seekerPosition = newValue) } progressUpdaterJob?.cancel()
}
override fun seekingFinished() {
resetHideUiDelayedJob()
val seekerPosition = mutableUiState.value.seekerPosition val seekerPosition = mutableUiState.value.seekerPosition
val seekPositionInMs = (newPlayer?.duration?.toFloat() ?: 0F) * seekerPosition val seekPositionInMs = (newPlayer?.duration?.toFloat() ?: 0F) * seekerPosition
newPlayer?.currentPosition = seekPositionInMs.toLong() newPlayer?.currentPosition = seekPositionInMs.toLong()
Log.i(TAG, "Seek to Ms: $seekPositionInMs") 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) { override fun embeddedDraggedDown(offset: Float) {
@ -461,30 +475,37 @@ class NewPlayerViewModelImpl @Inject constructor(
} }
override fun fastSeek(count: Int) { override fun fastSeek(count: Int) {
if(abs(count) == 1) {
playbackPositionWhenFastSeekStarted = newPlayer?.currentPosition ?: 0
}
val fastSeekAmountInS = count * (newPlayer?.fastSeekAmountSec ?: 10)
mutableUiState.update { mutableUiState.update {
it.copy( it.copy(
fastSeekSeconds = count * (newPlayer?.fastSeekAmountSec ?: 10) fastSeekSeconds = fastSeekAmountInS
) )
} }
if (fastSeekAmountInS != 0) {
Log.d(TAG, "fast seeking seeking by $fastSeekAmountInS seconds")
newPlayer?.currentPosition =
playbackPositionWhenFastSeekStarted + (fastSeekAmountInS * 1000)
}
if (mutableUiState.value.uiMode.videoControllerUiVisible) { if (mutableUiState.value.uiMode.videoControllerUiVisible) {
resetHideUiDelayedJob() startHideUiDelayedJob()
} }
} }
override fun finishFastSeek() { override fun finishFastSeek() {
if (mutableUiState.value.uiMode.videoControllerUiVisible) { if (mutableUiState.value.uiMode.videoControllerUiVisible) {
resetHideUiDelayedJob() startHideUiDelayedJob()
} }
mutableUiState.update {
val fastSeekAmount = mutableUiState.value.fastSeekSeconds it.copy(fastSeekSeconds = 0)
if (fastSeekAmount != 0) {
Log.d(TAG, "$fastSeekAmount")
newPlayer?.currentPosition = (newPlayer?.currentPosition ?: 0) + (fastSeekAmount * 1000)
mutableUiState.update {
it.copy(fastSeekSeconds = 0)
}
} }
} }
@ -522,7 +543,7 @@ class NewPlayerViewModelImpl @Inject constructor(
override fun onBackPressed() { override fun onBackPressed() {
val nextMode = uiState.value.uiMode.getNextModeWhenBackPressed() val nextMode = uiState.value.uiMode.getNextModeWhenBackPressed()
if (nextMode != null) { if (nextMode != null) {
updateUiMode(nextMode) changeUiMode(nextMode, null)
} else { } else {
safeTryEmit(mutableOnBackPressed, Unit) safeTryEmit(mutableOnBackPressed, Unit)
} }
@ -570,7 +591,7 @@ class NewPlayerViewModelImpl @Inject constructor(
playList = tempList playList = tempList
) )
} }
resetPlaylistProgressUpdaterJob() startPlaylistProgressUpdaterJob()
} }
override fun onStreamItemDragFinished() { override fun onStreamItemDragFinished() {
@ -582,9 +603,9 @@ class NewPlayerViewModelImpl @Inject constructor(
override fun dialogVisible(visible: Boolean) { override fun dialogVisible(visible: Boolean) {
if (visible) { if (visible) {
uiVisibilityJob?.cancel() hideUiDelayedJob?.cancel()
} else { } else {
resetHideUiDelayedJob() startHideUiDelayedJob()
} }
} }
@ -592,21 +613,6 @@ class NewPlayerViewModelImpl @Inject constructor(
newPlayer?.removePlaylistItem(uniqueId) newPlayer?.removePlaylistItem(uniqueId)
} }
private fun updateUiMode(newState: UIModeState) {
val newPlayMode = newState.toPlayMode()
val currentPlayMode = mutableUiState.value.uiMode.toPlayMode()
if (newPlayMode != currentPlayMode) {
newPlayer?.playBackMode?.update {
newPlayMode!!
}
} else {
mutableUiState.update {
it.copy(uiMode = newState)
}
}
}
private fun getEmbeddedUiRatio() = newPlayer?.exoPlayer?.value?.let { player -> private fun getEmbeddedUiRatio() = newPlayer?.exoPlayer?.value?.let { player ->
val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio() val videoRatio = VideoSize.fromMedia3VideoSize(player.videoSize).getRatio()
return (if (videoRatio.isNaN()) currentContentRatio return (if (videoRatio.isNaN()) currentContentRatio

View File

@ -98,6 +98,24 @@ enum class UIModeState {
else -> false else -> false
} }
val requiresProgressUpdate: Boolean
get() =
when(this) {
PLACEHOLDER -> false
EMBEDDED_VIDEO -> false
EMBEDDED_VIDEO_CONTROLLER_UI -> true
EMBEDDED_VIDEO_CHAPTER_SELECT -> false
EMBEDDED_VIDEO_STREAM_SELECT -> false
FULLSCREEN_VIDEO -> false
FULLSCREEN_VIDEO_CONTROLLER_UI -> true
FULLSCREEN_VIDEO_CHAPTER_SELECT -> false
FULLSCREEN_VIDEO_STREAM_SELECT -> false
EMBEDDED_AUDIO -> true
FULLSCREEN_AUDIO -> true
AUDIO_CHAPTER_SELECT -> false
AUDIO_STREAM_SELECT -> false
}
// STATE TRANSITIONS // STATE TRANSITIONS
fun getControllerUiVisibleState() = fun getControllerUiVisibleState() =

View File

@ -22,6 +22,7 @@ package net.newpipe.newplayer.ui
import android.app.Activity import android.app.Activity
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.os.Build
import android.util.Log import android.util.Log
import android.view.SurfaceView import android.view.SurfaceView
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
@ -30,6 +31,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.adaptive.currentWindowSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -54,6 +56,7 @@ import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.videoplayer.VideoPlayerUi import net.newpipe.newplayer.ui.videoplayer.VideoPlayerUi
import net.newpipe.newplayer.utils.LockScreenOrientation import net.newpipe.newplayer.utils.LockScreenOrientation
import net.newpipe.newplayer.utils.getDefaultBrightness import net.newpipe.newplayer.utils.getDefaultBrightness
import net.newpipe.newplayer.utils.isInPowerSaveMode
import net.newpipe.newplayer.utils.setScreenBrightness import net.newpipe.newplayer.utils.setScreenBrightness
private const val TAG = "VideoPlayerUI" private const val TAG = "VideoPlayerUI"
@ -112,6 +115,13 @@ fun NewPlayerUI(
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val isInPowerSaveMode = isInPowerSaveMode()
LaunchedEffect(key1 = isInPowerSaveMode) {
viewModel.deviceInPowerSaveMode = isInPowerSaveMode
}
}
if (uiState.uiMode.fitScreenRotation) { if (uiState.uiMode.fitScreenRotation) {
if (uiState.contentRatio < 1) { if (uiState.contentRatio < 1) {
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
@ -142,10 +152,13 @@ fun NewPlayerUI(
) { ) {
VideoPlayerUi(viewModel = viewModel, uiState = uiState) VideoPlayerUi(viewModel = viewModel, uiState = uiState)
} else if (uiState.uiMode == UIModeState.FULLSCREEN_AUDIO || } else if (uiState.uiMode == UIModeState.FULLSCREEN_AUDIO ||
uiState.uiMode == UIModeState.EMBEDDED_AUDIO ||
uiState.uiMode == UIModeState.AUDIO_STREAM_SELECT || uiState.uiMode == UIModeState.AUDIO_STREAM_SELECT ||
uiState.uiMode == UIModeState.AUDIO_CHAPTER_SELECT uiState.uiMode == UIModeState.AUDIO_CHAPTER_SELECT
) { ) {
AudioPlayerUI(viewModel = viewModel, uiState = uiState) val windowSize = currentWindowSize()
AudioPlayerUI(viewModel = viewModel, uiState = uiState,
isLandScape = windowSize.height < windowSize.width)
} else { } else {
LoadingPlaceholder(uiState.embeddedUiRatio) LoadingPlaceholder(uiState.embeddedUiRatio)
} }

View File

@ -57,19 +57,24 @@ import net.newpipe.newplayer.ui.LoadingPlaceholder
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
@androidx.annotation.OptIn(UnstableApi::class) @androidx.annotation.OptIn(UnstableApi::class)
@OptIn(UnstableApi::class)
@Composable @Composable
fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) { fun AudioPlaybackController(
modifier: Modifier = Modifier,
viewModel: NewPlayerViewModel,
uiState: NewPlayerUIState
) {
Row( Row(
modifier = Modifier.background(MaterialTheme.colorScheme.background), modifier = modifier.background(MaterialTheme.colorScheme.background),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
//ShuffleModeButton(viewModel = viewModel, uiState = uiState) //ShuffleModeButton(viewModel = viewModel, uiState = uiState)
Box(modifier = Modifier Box(
.fillMaxWidth() modifier = Modifier
.aspectRatio(1F) .fillMaxWidth()
.weight(1F), contentAlignment = Alignment.Center) { .aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center
) {
Button( Button(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -87,10 +92,12 @@ fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIS
} }
} }
Box(modifier = Modifier Box(
.fillMaxWidth() modifier = Modifier
.aspectRatio(1F) .fillMaxWidth()
.weight(1F), contentAlignment = Alignment.Center) { .aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center
) {
Button( Button(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -114,11 +121,14 @@ fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIS
onClick = if (uiState.playing) viewModel::pause else viewModel::play, onClick = if (uiState.playing) viewModel::pause else viewModel::play,
shape = CircleShape shape = CircleShape
) { ) {
if(uiState.isLoading) { if (uiState.isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.fillMaxSize().aspectRatio(1F), modifier = Modifier
color = MaterialTheme.colorScheme.onSurface) .fillMaxSize()
.aspectRatio(1F),
color = MaterialTheme.colorScheme.onSurface
)
} }
} else { } else {
@ -133,10 +143,12 @@ fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIS
} }
} }
Box(modifier = Modifier Box(
.fillMaxWidth() modifier = Modifier
.aspectRatio(1F) .fillMaxWidth()
.weight(1F), contentAlignment = Alignment.Center) { .aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center
) {
Button( Button(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -155,10 +167,12 @@ fun AudioPlaybackController(viewModel: NewPlayerViewModel, uiState: NewPlayerUIS
} }
} }
Box(modifier = Modifier Box(
.fillMaxWidth() modifier = Modifier
.aspectRatio(1F) .fillMaxWidth()
.weight(1F), contentAlignment = Alignment.Center) { .aspectRatio(1F)
.weight(1F), contentAlignment = Alignment.Center
) {
Button( Button(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@ -0,0 +1,181 @@
/* 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.ui.audioplayer
import android.app.Activity
import androidx.annotation.OptIn
import androidx.collection.emptyLongSet
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
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.model.EmbeddedUiConfig
import net.newpipe.newplayer.model.NewPlayerUIState
import net.newpipe.newplayer.model.NewPlayerViewModel
import net.newpipe.newplayer.model.NewPlayerViewModelDummy
import net.newpipe.newplayer.model.UIModeState
import net.newpipe.newplayer.ui.selection_ui.ITEM_CORNER_SHAPE
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.videoplayer.CONTROLLER_UI_BACKGROUND_COLOR
import net.newpipe.newplayer.ui.videoplayer.PreviewBackgroundSurface
import net.newpipe.newplayer.utils.Thumbnail
import net.newpipe.newplayer.utils.getEmbeddedUiConfig
import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs
@OptIn(androidx.media3.common.util.UnstableApi::class)
@Composable
fun AudioPlayerEmbeddedUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
val locale = getLocale()!!
val embeddedUIConfig = if (LocalContext.current is Activity)
getEmbeddedUiConfig(activity = LocalContext.current as Activity)
else
EmbeddedUiConfig.DUMMY
Box(modifier = Modifier.wrapContentSize()) {
Thumbnail(
modifier = Modifier.fillMaxWidth(),
thumbnail = uiState.currentlyPlaying?.mediaMetadata?.artworkUri,
contentDescription = stringResource(
id = R.string.stream_thumbnail
)
)
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomStart)
) {
Surface(
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.wrapContentSize()
.padding(start = 10.dp, bottom = 14.dp)
) {
Text(
modifier = Modifier.padding(
start = 4.dp,
end = 4.dp,
top = 0.5.dp,
bottom = 0.5.dp
),
text = getTimeStringFromMs(
uiState.playbackPositionInMs,
locale,
leadingZerosForMinutes = false
),
fontSize = 14.sp,
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
)
Surface(
color = CONTROLLER_UI_BACKGROUND_COLOR,
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.wrapContentSize()
.padding(bottom = 14.dp, end = 10.dp)
) {
Text(
modifier = Modifier.padding(
start = 4.dp,
end = 4.dp,
top = 0.5.dp,
bottom = 0.5.dp
),
color = MaterialTheme.colorScheme.onBackground,
text = getTimeStringFromMs(
uiState.durationInMs,
locale,
leadingZerosForMinutes = false
),
fontSize = 14.sp,
)
}
}
LinearProgressIndicator(modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth(),
progress = {
val duration = if (uiState.durationInMs == 0L) {
0.000000001f
} else {
uiState.durationInMs.toFloat()
}
(uiState.playbackPositionInMs.toFloat() / duration)
})
Surface(
modifier = Modifier
.matchParentSize()
.clickable {
viewModel.changeUiMode(
UIModeState.FULLSCREEN_AUDIO,
embeddedUiConfig = embeddedUIConfig
)
}, color = Color.Transparent
) {
}
}
}
@OptIn(UnstableApi::class)
@Preview(device = "spec:width=1080px,height=1080px,dpi=440,orientation=landscape")
@Composable
fun AuidioPlayerEmbeddedPreview() {
VideoPlayerTheme {
PreviewBackgroundSurface {
AudioPlayerEmbeddedUI(
viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY,
)
}
}
}

View File

@ -21,27 +21,36 @@
package net.newpipe.newplayer.ui.audioplayer package net.newpipe.newplayer.ui.audioplayer
import android.icu.text.CaseMap.Title
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -59,6 +68,8 @@ import net.newpipe.newplayer.ui.selection_ui.StreamSelectUI
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.utils.Thumbnail import net.newpipe.newplayer.utils.Thumbnail
import net.newpipe.newplayer.utils.getInsets import net.newpipe.newplayer.utils.getInsets
import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs
private val UI_ENTER_ANIMATION = fadeIn(tween(200)) private val UI_ENTER_ANIMATION = fadeIn(tween(200))
@ -73,11 +84,12 @@ fun lightAudioControlButtonColorScheme() = ButtonDefaults.buttonColors().copy(
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Composable @Composable
fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) { fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState, isLandScape: Boolean) {
val insets = getInsets() val insets = getInsets()
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .wrapContentSize()
.background(color = MaterialTheme.colorScheme.background) .background(color = MaterialTheme.colorScheme.background)
) { ) {
AnimatedVisibility( AnimatedVisibility(
@ -96,6 +108,14 @@ fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
StreamSelectUI(viewModel = viewModel, uiState = uiState, shownInAudioPlayer = true) StreamSelectUI(viewModel = viewModel, uiState = uiState, shownInAudioPlayer = true)
} }
AnimatedVisibility(
visible = uiState.uiMode == UIModeState.EMBEDDED_AUDIO,
enter = UI_ENTER_ANIMATION,
exit = UI_EXIT_ANIMATION
) {
AudioPlayerEmbeddedUI(viewModel = viewModel, uiState = uiState)
}
AnimatedVisibility( AnimatedVisibility(
uiState.uiMode == UIModeState.FULLSCREEN_AUDIO, uiState.uiMode == UIModeState.FULLSCREEN_AUDIO,
enter = UI_ENTER_ANIMATION, enter = UI_ENTER_ANIMATION,
@ -107,94 +127,254 @@ fun AudioPlayerUI(viewModel: NewPlayerViewModel, uiState: NewPlayerUIState) {
topBar = { topBar = {
}) { innerPadding -> }) { innerPadding ->
Box( if (isLandScape) {
modifier = Modifier LandscapeLayout(
.fillMaxSize() viewModel = viewModel,
.padding(innerPadding) uiState = uiState,
) { innerPadding = innerPadding
Column( )
modifier = Modifier } else {
.fillMaxSize() PortraitLayout(
) { viewModel = viewModel,
Column( uiState = uiState,
modifier = Modifier innerPadding = innerPadding
.fillMaxSize() )
.padding(20.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
)
Box {
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
Thumbnail(
modifier = Modifier.fillMaxWidth(),
thumbnail = uiState.currentlyPlaying?.mediaMetadata?.artworkUri,
contentDescription = stringResource(
id = R.string.stream_thumbnail
),
)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
)
Text(
text = uiState.currentlyPlaying?.mediaMetadata?.title.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 6.em
)
Text(
text = uiState.currentlyPlaying?.mediaMetadata?.artist.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 4.em
)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.2f)
)
NewPlayerSeeker(viewModel = viewModel, uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.2f)
)
AudioPlaybackController(viewModel = viewModel, uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.2f)
)
}
AudioBottomUI(viewModel, uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.025f)
)
}
} }
} }
} }
} }
} }
@OptIn(UnstableApi::class)
@Composable
private fun LandscapeLayout(
modifier: Modifier = Modifier,
viewModel: NewPlayerViewModel,
uiState: NewPlayerUIState,
innerPadding: PaddingValues
) {
Row(modifier = modifier
.fillMaxSize()
.padding(20.dp)) {
Column(
modifier = Modifier
.fillMaxSize()
.weight(1f)
) {
CoverArt(modifier = Modifier
.fillMaxSize()
.weight(0.9f), uiState = uiState)
TitleView(modifier = Modifier
.fillMaxSize()
.weight(0.1f), uiState = uiState)
}
Box(modifier = Modifier.width(20.dp))
Column(
modifier = Modifier
.fillMaxSize()
.weight(1f)
) {
Column(
verticalArrangement = Arrangement.SpaceAround,
modifier = Modifier
.fillMaxSize()
.weight(1f)
) {
ProgressUI(
viewModel = viewModel,
uiState = uiState
)
AudioPlaybackController(
viewModel = viewModel,
uiState = uiState
)
}
AudioBottomUI(viewModel, uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.025f)
)
}
}
}
@OptIn(UnstableApi::class)
@Composable
private fun PortraitLayout(
modifier: Modifier = Modifier,
viewModel: NewPlayerViewModel,
uiState: NewPlayerUIState,
innerPadding: PaddingValues
) {
Box(
modifier = modifier
.fillMaxSize()
.padding(innerPadding)
) {
Column(
modifier = Modifier
.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
)
CoverArt(uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.3f)
)
TitleView(uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.6f)
)
ProgressUI(viewModel = viewModel, uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.2f)
)
AudioPlaybackController(viewModel = viewModel, uiState = uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.2f)
)
}
AudioBottomUI(viewModel, uiState)
Box(
modifier = Modifier
.fillMaxSize()
.weight(0.025f)
)
}
}
}
@OptIn(UnstableApi::class)
@Composable
private fun ProgressUI(
modifier: Modifier = Modifier,
viewModel: NewPlayerViewModel,
uiState: NewPlayerUIState
) {
val locale = getLocale()!!
Column(modifier = modifier) {
NewPlayerSeeker(viewModel = viewModel, uiState = uiState)
Row {
Text(
getTimeStringFromMs(
uiState.playbackPositionInMs,
getLocale() ?: locale
)
)
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
)
Text(
getTimeStringFromMs(
uiState.durationInMs,
getLocale() ?: locale
)
)
}
}
}
@OptIn(UnstableApi::class)
@Composable
private fun TitleView(modifier: Modifier = Modifier, uiState: NewPlayerUIState) {
Column(modifier = modifier) {
Text(
text = uiState.currentlyPlaying?.mediaMetadata?.title.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 6.em
)
Text(
text = uiState.currentlyPlaying?.mediaMetadata?.artist.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
fontSize = 4.em
)
}
}
@OptIn(UnstableApi::class)
@Composable
private fun CoverArt(modifier: Modifier = Modifier, uiState: NewPlayerUIState) {
Box {
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
Thumbnail(
modifier = Modifier.fillMaxWidth(),
thumbnail = uiState.currentlyPlaying?.mediaMetadata?.artworkUri,
contentDescription = stringResource(
id = R.string.stream_thumbnail
),
)
}
}
}
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Preview(device = "id:pixel_6", showSystemUi = true) @Preview(device = "id:pixel_6", showSystemUi = true)
@Composable @Composable
fun AudioPlayerUIPreview() { fun AudioPlayerUIPortraitPreview() {
VideoPlayerTheme { VideoPlayerTheme {
AudioPlayerUI(viewModel = NewPlayerViewModelDummy(), uiState = NewPlayerUIState.DUMMY) AudioPlayerUI(
viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY.copy(uiMode = UIModeState.FULLSCREEN_AUDIO),
isLandScape = false
)
}
}
@OptIn(UnstableApi::class)
@Preview(device = "spec:parent=pixel_6,orientation=landscape", showSystemUi = true)
@Composable
fun AudioPlayerUILandscapePreview() {
VideoPlayerTheme {
AudioPlayerUI(
viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY.copy(uiMode = UIModeState.FULLSCREEN_AUDIO),
isLandScape = true
)
}
}
@OptIn(UnstableApi::class)
@Preview(device = "spec:parent=pixel_6,orientation=portrait", showSystemUi = true)
@Composable
fun AudioPlayerUIEmbeddedPreview() {
VideoPlayerTheme {
AudioPlayerUI(
viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY.copy(uiMode = UIModeState.EMBEDDED_AUDIO),
isLandScape = false
)
} }
} }

View File

@ -56,7 +56,9 @@ val video_player_onErrorContainer = Color(0xFFFFDAD6)
// background color // background color
val video_player_background = Color(0xFF1F1B16) val video_player_background = Color(0xFF1F1B16)
// Font color on background
val video_player_onBackground = Color(0xFFEAE1D9) val video_player_onBackground = Color(0xFFEAE1D9)
val video_player_surface = Color(0xFF000000) val video_player_surface = Color(0xFF000000)
// The color of the Text and icons // The color of the Text and icons
@ -104,13 +106,13 @@ fun VideoPlayerControllerUIPreviewEmbeddedColorPreview() {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Preview(device = "id:pixel_6") @Preview(device = "id:pixel_6")
@Composable @Composable
fun AudioPlayerUIPreviewEmbeddedColorPreview() { fun AudioPlayerUIColorPreview() {
VideoPlayerTheme { VideoPlayerTheme {
PreviewBackgroundSurface { PreviewBackgroundSurface {
AudioPlayerUI( AudioPlayerUI(
viewModel = NewPlayerViewModelDummy(), viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY.copy( uiState = NewPlayerUIState.DUMMY.copy(
uiMode = UIModeState.EMBEDDED_VIDEO_CONTROLLER_UI, uiMode = UIModeState.FULLSCREEN_AUDIO,
playing = true, playing = true,
seekerPosition = 0.3f, seekerPosition = 0.3f,
isLoading = false, isLoading = false,
@ -119,6 +121,7 @@ fun AudioPlayerUIPreviewEmbeddedColorPreview() {
bufferedPercentage = 0.4f, bufferedPercentage = 0.4f,
fastSeekSeconds = 10, fastSeekSeconds = 10,
), ),
isLandScape = false
) )
} }
} }

View File

@ -23,7 +23,9 @@ package net.newpipe.newplayer.ui.videoplayer.controller
import android.app.Activity import android.app.Activity
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Fullscreen import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.filled.FullscreenExit import androidx.compose.material.icons.filled.FullscreenExit
@ -38,6 +40,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
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.compose.ui.unit.dp
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.EmbeddedUiConfig import net.newpipe.newplayer.model.EmbeddedUiConfig
@ -67,7 +70,13 @@ fun BottomUI(
val locale = getLocale()!! val locale = getLocale()!!
Text(getTimeStringFromMs(uiState.playbackPositionInMs, getLocale() ?: locale)) Text(getTimeStringFromMs(uiState.playbackPositionInMs, getLocale() ?: locale))
NewPlayerSeeker(modifier = Modifier.weight(1F), viewModel = viewModel, uiState = uiState) NewPlayerSeeker(
modifier = Modifier
.weight(1F)
.padding(start = 4.dp, end = 4.dp),
viewModel = viewModel,
uiState = uiState
)
Text(getTimeStringFromMs(uiState.durationInMs, getLocale() ?: locale)) Text(getTimeStringFromMs(uiState.durationInMs, getLocale() ?: locale))
@ -112,7 +121,7 @@ fun VideoPlayerControllerBottomUIPreview() {
viewModel = NewPlayerViewModelDummy(), viewModel = NewPlayerViewModelDummy(),
uiState = NewPlayerUIState.DUMMY.copy( uiState = NewPlayerUIState.DUMMY.copy(
uiMode = UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI, uiMode = UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI,
seekerPosition = 0.2f, seekerPosition = 0.0f,
playbackPositionInMs = 3 * 60 * 1000, playbackPositionInMs = 3 * 60 * 1000,
bufferedPercentage = 0.4f bufferedPercentage = 0.4f
), ),

View File

@ -87,9 +87,9 @@ fun EmbeddedGestureUI(
val defaultOnRegularTap = { val defaultOnRegularTap = {
if (uiState.uiMode.videoControllerUiVisible) { if (uiState.uiMode.videoControllerUiVisible) {
viewModel.hideUi() viewModel.changeUiMode(uiState.uiMode.getUiHiddenState(), null)
} else { } else {
viewModel.showUi() viewModel.changeUiMode(uiState.uiMode.getControllerUiVisibleState(), null)
} }
} }
@ -124,7 +124,7 @@ fun EmbeddedGestureUI(
if (count == 1) { if (count == 1) {
if (uiState.playing) { if (uiState.playing) {
viewModel.pause() viewModel.pause()
viewModel.showUi() viewModel.changeUiMode(uiState.uiMode.getControllerUiVisibleState(), null)
} else { } else {
viewModel.play() viewModel.play()
} }

View File

@ -36,6 +36,8 @@ 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
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -46,6 +48,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.model.EmbeddedUiConfig
import net.newpipe.newplayer.model.UIModeState import net.newpipe.newplayer.model.UIModeState
import net.newpipe.newplayer.model.NewPlayerUIState import net.newpipe.newplayer.model.NewPlayerUIState
import net.newpipe.newplayer.model.NewPlayerViewModel import net.newpipe.newplayer.model.NewPlayerViewModel
@ -55,7 +58,7 @@ import net.newpipe.newplayer.utils.getDefaultBrightness
import net.newpipe.newplayer.utils.getEmbeddedUiConfig import net.newpipe.newplayer.utils.getEmbeddedUiConfig
private enum class IndicatorMode { private enum class IndicatorMode {
NONE, VOLUME_INDICATOR_VISSIBLE, BRIGHTNESS_INDICATOR_VISSIBLE NONE, VOLUME_INDICATOR_VISIBLE, BRIGHTNESS_INDICATOR_VISIBLE
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@ -65,7 +68,7 @@ fun FullscreenGestureUI(
) { ) {
var heightPx by remember { var heightPx by remember {
mutableStateOf(0f) mutableFloatStateOf(0f)
} }
var indicatorMode by remember { var indicatorMode by remember {
@ -74,9 +77,9 @@ fun FullscreenGestureUI(
val defaultOnRegularTap = { val defaultOnRegularTap = {
if (uiState.uiMode.videoControllerUiVisible) { if (uiState.uiMode.videoControllerUiVisible) {
viewModel.hideUi() viewModel.changeUiMode(uiState.uiMode.getUiHiddenState(), null)
} else { } else {
viewModel.showUi() viewModel.changeUiMode(uiState.uiMode.getControllerUiVisibleState(), null)
} }
} }
@ -100,8 +103,8 @@ fun FullscreenGestureUI(
indicatorMode = IndicatorMode.NONE indicatorMode = IndicatorMode.NONE
}, },
onMovement = { change -> onMovement = { change ->
if (indicatorMode == IndicatorMode.NONE || indicatorMode == IndicatorMode.BRIGHTNESS_INDICATOR_VISSIBLE) { if (indicatorMode == IndicatorMode.NONE || indicatorMode == IndicatorMode.BRIGHTNESS_INDICATOR_VISIBLE) {
indicatorMode = IndicatorMode.BRIGHTNESS_INDICATOR_VISSIBLE indicatorMode = IndicatorMode.BRIGHTNESS_INDICATOR_VISIBLE
if (heightPx != 0f) { if (heightPx != 0f) {
viewModel.brightnessChange(-change.y / heightPx, defaultBrightness) viewModel.brightnessChange(-change.y / heightPx, defaultBrightness)
@ -131,7 +134,7 @@ fun FullscreenGestureUI(
if(count == 1) { if(count == 1) {
if(uiState.playing) { if(uiState.playing) {
viewModel.pause() viewModel.pause()
viewModel.showUi() viewModel.changeUiMode(uiState.uiMode.getControllerUiVisibleState(), null)
} else { } else {
viewModel.play() viewModel.play()
} }
@ -145,8 +148,8 @@ fun FullscreenGestureUI(
indicatorMode = IndicatorMode.NONE indicatorMode = IndicatorMode.NONE
}, },
onMovement = { change -> onMovement = { change ->
if (indicatorMode == IndicatorMode.NONE || indicatorMode == IndicatorMode.VOLUME_INDICATOR_VISSIBLE) { if (indicatorMode == IndicatorMode.NONE || indicatorMode == IndicatorMode.VOLUME_INDICATOR_VISIBLE) {
indicatorMode = IndicatorMode.VOLUME_INDICATOR_VISSIBLE indicatorMode = IndicatorMode.VOLUME_INDICATOR_VISIBLE
if (heightPx != 0f) { if (heightPx != 0f) {
viewModel.volumeChange(-change.y / heightPx) viewModel.volumeChange(-change.y / heightPx)
} }
@ -166,14 +169,14 @@ fun FullscreenGestureUI(
IndicatorAnimation( IndicatorAnimation(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
visible = indicatorMode == IndicatorMode.VOLUME_INDICATOR_VISSIBLE, visible = indicatorMode == IndicatorMode.VOLUME_INDICATOR_VISIBLE,
) { ) {
VolumeCircle(volumeFraction = uiState.soundVolume) VolumeCircle(volumeFraction = uiState.soundVolume)
} }
IndicatorAnimation( IndicatorAnimation(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
visible = indicatorMode == IndicatorMode.BRIGHTNESS_INDICATOR_VISSIBLE, visible = indicatorMode == IndicatorMode.BRIGHTNESS_INDICATOR_VISIBLE,
) { ) {
VolumeCircle( VolumeCircle(
volumeFraction = uiState.brightness ?: defaultBrightness, volumeFraction = uiState.brightness ?: defaultBrightness,
@ -243,15 +246,15 @@ fun FullscreenGestureUIPreview() {
fun FullscreenGestureUIPreviewInteractive() { fun FullscreenGestureUIPreviewInteractive() {
var seekSeconds by remember { var seekSeconds by remember {
mutableStateOf(0) mutableIntStateOf(0)
} }
var brightnessValue by remember { var brightnessValue by remember {
mutableStateOf(0f) mutableFloatStateOf(0f)
} }
var soundVolume by remember { var soundVolume by remember {
mutableStateOf(0f) mutableFloatStateOf(0f)
} }
var uiVisible by remember { var uiVisible by remember {
@ -264,12 +267,12 @@ fun FullscreenGestureUIPreviewInteractive() {
modifier = Modifier, modifier = Modifier,
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
object : NewPlayerViewModelDummy() { object : NewPlayerViewModelDummy() {
override fun hideUi() { override fun changeUiMode(
uiVisible = false newUiModeState: UIModeState,
} embeddedUiConfig: EmbeddedUiConfig?
) {
override fun showUi() { super.changeUiMode(newUiModeState, embeddedUiConfig)
uiVisible = true uiVisible = newUiModeState.videoControllerUiVisible
} }
override fun fastSeek(steps: Int) { override fun fastSeek(steps: Int) {
@ -299,7 +302,7 @@ fun FullscreenGestureUIPreviewInteractive() {
} }
AnimatedVisibility(uiVisible) { AnimatedVisibility(uiVisible) {
Text("UI is Vissible") Text("UI is Visible")
} }
} }
} }

View File

@ -1,3 +1,24 @@
/* 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.utils package net.newpipe.newplayer.utils
data class VideoSize( data class VideoSize(

View File

@ -24,25 +24,23 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.graphics.drawable.shapes.Shape
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.view.WindowManager import android.view.WindowManager
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis import androidx.annotation.RequiresApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.waterfall import androidx.compose.foundation.layout.waterfall
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
@ -184,6 +182,13 @@ fun Thumbnail(
} }
} }
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@Composable
fun isInPowerSaveMode() =
(LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager)
.isPowerSaveMode
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun getPlaylistDurationInMS(playlist: List<MediaItem>) : Long { fun getPlaylistDurationInMS(playlist: List<MediaItem>) : Long {
var duration = 0L var duration = 0L

View File

@ -26,6 +26,7 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.update
import net.newpipe.newplayer.ActivityBrainSlug import net.newpipe.newplayer.ActivityBrainSlug
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.PlayMode import net.newpipe.newplayer.PlayMode
@ -53,42 +54,54 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
binding.start6502StreamButton.setOnClickListener { binding.buttons.start6502StreamButton.setOnClickListener {
newPlayer.playWhenReady = true newPlayer.playWhenReady = true
newPlayer.playStream("6502", PlayMode.EMBEDDED_VIDEO) newPlayer.playStream("6502", PlayMode.EMBEDDED_VIDEO)
} }
binding.startImuStreamButton.setOnClickListener { binding.buttons.startImuStreamButton.setOnClickListener {
newPlayer.playWhenReady = true newPlayer.playWhenReady = true
newPlayer.playStream("imu", PlayMode.EMBEDDED_VIDEO) newPlayer.playStream("imu", PlayMode.EMBEDDED_VIDEO)
} }
binding.startPortraitStreamButton.setOnClickListener { binding.buttons.startPortraitStreamButton.setOnClickListener {
newPlayer.playWhenReady = true newPlayer.playWhenReady = true
newPlayer.playStream("portrait", PlayMode.EMBEDDED_VIDEO) newPlayer.playStream("portrait", PlayMode.EMBEDDED_VIDEO)
} }
binding.startYtTestVideoButton.setOnClickListener { binding.buttons.startYtTestVideoButton.setOnClickListener {
newPlayer.playWhenReady = true newPlayer.playWhenReady = true
newPlayer.playStream("yt_test", PlayMode.EMBEDDED_VIDEO) newPlayer.playStream("yt_test", PlayMode.EMBEDDED_VIDEO)
} }
binding.add6502StreamButton.setOnClickListener { binding.buttons.add6502StreamButton.setOnClickListener {
newPlayer.addToPlaylist("6502") newPlayer.addToPlaylist("6502")
} }
binding.addImuStreamButton.setOnClickListener { binding.buttons.addImuStreamButton.setOnClickListener {
newPlayer.addToPlaylist("imu") newPlayer.addToPlaylist("imu")
} }
binding.addPortraitStreamButton.setOnClickListener { binding.buttons.addPortraitStreamButton.setOnClickListener {
newPlayer.addToPlaylist("portrait") newPlayer.addToPlaylist("portrait")
} }
binding.addYtTestVideoButton.setOnClickListener { binding.buttons.addYtTestVideoButton.setOnClickListener {
newPlayer.addToPlaylist("yt_test") newPlayer.addToPlaylist("yt_test")
} }
binding.buttons.listenModeButton.setOnClickListener {
newPlayer.playBackMode.update {
PlayMode.FULLSCREEN_AUDIO
}
}
binding.buttons.pipModeButton.setOnClickListener {
newPlayer.playBackMode.update {
PlayMode.PIP
}
}
newPlayerViewModel.newPlayer = newPlayer newPlayerViewModel.newPlayer = newPlayer
newPlayerViewModel.contentFitMode = ContentScale.FIT_INSIDE newPlayerViewModel.contentFitMode = ContentScale.FIT_INSIDE

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="#e8eaed" android:pathData="M360,840L200,840q-33,0 -56.5,-23.5T120,760v-280q0,-75 28.5,-140.5t77,-114q48.5,-48.5 114,-77T480,120q75,0 140.5,28.5t114,77q48.5,48.5 77,114T840,480v280q0,33 -23.5,56.5T760,840L600,840v-320h160v-40q0,-117 -81.5,-198.5T480,200q-117,0 -198.5,81.5T200,480v40h160v320ZM280,600h-80v160h80v-160ZM680,600v160h80v-160h-80ZM280,600h-80,80ZM680,600h80,-80Z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="#e8eaed" android:pathData="M160,800q-33,0 -56.5,-23.5T80,720v-480q0,-33 23.5,-56.5T160,160h640q33,0 56.5,23.5T880,240v480q0,33 -23.5,56.5T800,800L160,800ZM160,720h640v-480L160,240v480ZM160,720v-480,480ZM440,520h320v-240L440,280v240ZM520,440v-80h160v80L520,440Z"/>
</vector>

View File

@ -73,73 +73,11 @@
app:layout_constraintStart_toEndOf="@id/embedded_player_layout" app:layout_constraintStart_toEndOf="@id/embedded_player_layout"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<LinearLayout <include
android:layout_width="match_parent" android:id="@+id/buttons"
android:layout_height="wrap_content" layout="@layout/buttons" />
android:orientation="vertical">
<Button
android:id="@+id/start_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start 6502 Stream" />
<Button
android:id="@+id/start_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start IMU Stream" />
<Button
android:id="@+id/start_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Portrait Stream" />
<Button
android:id="@+id/start_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Yt Test Video" />
<Button
android:id="@+id/add_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append 6502 Stream" />
<Button
android:id="@+id/add_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append IMU Stream" />
<Button
android:id="@+id/add_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append Portrait Stream" />
<Button
android:id="@+id/add_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Add Yt Test Video" />
</LinearLayout>
</ScrollView> </ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -40,14 +40,13 @@
<LinearLayout <LinearLayout
android:id="@+id/embedded_player_layout" android:id="@+id/embedded_player_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/buttons_layout" app:layout_constraintBottom_toTopOf="@+id/buttons_layout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" app:layout_constraintVertical_bias="0.0">
app:layout_constraintVertical_weight="1">
<net.newpipe.newplayer.VideoPlayerView <net.newpipe.newplayer.VideoPlayerView
android:id="@+id/embedded_player" android:id="@+id/embedded_player"
@ -72,73 +71,9 @@
app:layout_constraintTop_toBottomOf="@id/embedded_player_layout" app:layout_constraintTop_toBottomOf="@id/embedded_player_layout"
app:layout_constraintVertical_weight="1"> app:layout_constraintVertical_weight="1">
<LinearLayout <include
android:layout_width="match_parent" android:id="@+id/buttons"
android:layout_height="wrap_content" layout="@layout/buttons"/>
android:orientation="vertical">
<Button
android:id="@+id/start_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start 6502 Stream" />
<Button
android:id="@+id/start_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start IMU Stream" />
<Button
android:id="@+id/start_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Portrait Stream" />
<Button
android:id="@+id/start_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Yt Test Video" />
<Button
android:id="@+id/add_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append 6502 Stream" />
<Button
android:id="@+id/add_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append IMU Stream" />
<Button
android:id="@+id/add_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append Portrait Stream" />
<Button
android:id="@+id/add_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Add Yt Test Video" />
</LinearLayout>
</ScrollView> </ScrollView>

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<ImageButton
android:id="@+id/listen_mode_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/headphones"
android:contentDescription="Headphone mode" />
<ImageButton
android:id="@+id/pip_mode_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/pip"
android:contentDescription="Picture in Picture mode" />
</LinearLayout>
<Button
android:id="@+id/start_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start 6502 Stream" />
<Button
android:id="@+id/start_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start IMU Stream" />
<Button
android:id="@+id/start_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Portrait Stream" />
<Button
android:id="@+id/start_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start Yt Test Video" />
<Button
android:id="@+id/add_6502_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append 6502 Stream" />
<Button
android:id="@+id/add_imu_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append IMU Stream" />
<Button
android:id="@+id/add_portrait_stream_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Append Portrait Stream" />
<Button
android:id="@+id/add_yt_test_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Add Yt Test Video" />
</LinearLayout>

View File

@ -33,7 +33,6 @@
<item>1200000</item> <item>1200000</item>
<item>1800000</item> <item>1800000</item>
<item>2400000</item> <item>2400000</item>
<item>3600000</item>
</integer-array> </integer-array>
<integer name="ccc_6502_length">3116</integer> <integer name="ccc_6502_length">3116</integer>