start implementing NewPlayer interface

This commit is contained in:
Christian Schabesberger 2024-07-19 13:41:38 +02:00
parent f11d35818f
commit d526527e94
9 changed files with 148 additions and 107 deletions

View File

@ -21,12 +21,21 @@
package net.newpipe.newplayer package net.newpipe.newplayer
import android.app.Application import android.app.Application
import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
interface NewPlayer { interface NewPlayer {
val player: Player val player: Player
var playWhenReady: Boolean
fun prepare()
fun play()
fun pause()
//TODO: This is only temporary
fun setStream(uri: String)
data class Builder(val app: Application) { data class Builder(val app: Application) {
fun build(): NewPlayer { fun build(): NewPlayer {
@ -37,4 +46,31 @@ interface NewPlayer {
class NewPlayerImpl(internal_player: Player) : NewPlayer { class NewPlayerImpl(internal_player: Player) : NewPlayer {
override val player = internal_player override val player = internal_player
override var playWhenReady: Boolean
set(value) {
player.playWhenReady = value
}
get() = player.playWhenReady
override fun prepare() {
player.prepare()
}
override fun play() {
player.play()
}
override fun pause() {
player.pause()
}
override fun setStream(uri: String) {
if (player.playbackState == Player.STATE_IDLE) {
player.prepare()
}
player.setMediaItem(MediaItem.fromUri(uri))
}
} }

View File

@ -43,7 +43,11 @@ class VideoPlayerView : FrameLayout {
var minLayoutRatio: Float var minLayoutRatio: Float
get() = videoPlayerFragment.minLayoutRatio get() = videoPlayerFragment.minLayoutRatio
set(value) {videoPlayerFragment.maxLayoutRatio = value} set(value) {videoPlayerFragment.minLayoutRatio = value}
var newPlayer:NewPlayer?
set(value) {videoPlayerFragment.newPlayer = value}
get() = videoPlayerFragment.newPlayer
@JvmOverloads @JvmOverloads
constructor( constructor(

View File

@ -34,6 +34,7 @@ import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.internal.model.VideoPlayerViewModel import net.newpipe.newplayer.internal.model.VideoPlayerViewModel
import net.newpipe.newplayer.internal.model.VideoPlayerViewModelImpl import net.newpipe.newplayer.internal.model.VideoPlayerViewModelImpl
@ -48,6 +49,16 @@ class VideoPlayerFragment() : Fragment() {
private var currentVideoRatio = 0F private var currentVideoRatio = 0F
private lateinit var composeView: ComposeView private lateinit var composeView: ComposeView
var newPlayer: NewPlayer? = null
set(value) {
if(context != null) {
viewModel.newPlayer = value
} else {
field = value
}
}
get() = viewModel.newPlayer ?: field
var minLayoutRatio = 4F / 3F var minLayoutRatio = 4F / 3F
set(value) { set(value) {
if (value <= 0 && maxLayoutRatio < minLayoutRatio) if (value <= 0 && maxLayoutRatio < minLayoutRatio)
@ -87,6 +98,11 @@ class VideoPlayerFragment() : Fragment() {
val view = inflater.inflate(R.layout.video_player_framgent, container, false) val view = inflater.inflate(R.layout.video_player_framgent, container, false)
composeView = view.findViewById(R.id.player_copose_view) composeView = view.findViewById(R.id.player_copose_view)
// late init player in case player was set before fragment was attached to a context
if (viewModel.newPlayer == null) {
viewModel.newPlayer = newPlayer
}
viewModel.listener = object : VideoPlayerViewModel.Listener { viewModel.listener = object : VideoPlayerViewModel.Listener {
override fun requestUpdateLayoutRatio(videoRatio: Float) { override fun requestUpdateLayoutRatio(videoRatio: Float) {
currentVideoRatio = videoRatio currentVideoRatio = videoRatio
@ -103,15 +119,15 @@ class VideoPlayerFragment() : Fragment() {
} }
} }
viewModel.preparePlayer()
return view return view
} }
private fun updateViewRatio() { private fun updateViewRatio() {
composeView.updateLayoutParams<ConstraintLayout.LayoutParams> { if(this::composeView.isInitialized) {
val ratio = currentVideoRatio.coerceIn(minLayoutRatio, maxLayoutRatio) composeView.updateLayoutParams<ConstraintLayout.LayoutParams> {
dimensionRatio = "$ratio:1" val ratio = currentVideoRatio.coerceIn(minLayoutRatio, maxLayoutRatio)
dimensionRatio = "$ratio:1"
}
} }
} }
} }

View File

@ -28,13 +28,11 @@ import androidx.annotation.RequiresApi
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import net.newpipe.newplayer.R
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -61,14 +59,13 @@ data class VideoPlayerUIState(
} }
interface VideoPlayerViewModel { interface VideoPlayerViewModel {
val new_player: NewPlayer? var newPlayer: NewPlayer?
val player: Player? val player: Player?
val uiState: StateFlow<VideoPlayerUIState> val uiState: StateFlow<VideoPlayerUIState>
var listener: Listener? var listener: Listener?
val events: SharedFlow<Events>? val events: SharedFlow<Events>?
fun initUIState(instanceState: Bundle) fun initUIState(instanceState: Bundle)
fun preparePlayer()
fun play() fun play()
fun pause() fun pause()
fun prevStream() fun prevStream()
@ -90,12 +87,10 @@ interface VideoPlayerViewModel {
@HiltViewModel @HiltViewModel
class VideoPlayerViewModelImpl @Inject constructor( class VideoPlayerViewModelImpl @Inject constructor(
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
override val new_player: NewPlayer,
application: Application application: Application
) : AndroidViewModel(application), VideoPlayerViewModel { ) : AndroidViewModel(application), VideoPlayerViewModel {
// private // private
private val app = getApplication<Application>()
private val mutableUiState = MutableStateFlow(VideoPlayerUIState.DEFAULT) private val mutableUiState = MutableStateFlow(VideoPlayerUIState.DEFAULT)
private val mutableEvent = MutableSharedFlow<VideoPlayerViewModel.Events>() private val mutableEvent = MutableSharedFlow<VideoPlayerViewModel.Events>()
private var current_video_size = VideoSize.DEFAULT private var current_video_size = VideoSize.DEFAULT
@ -104,45 +99,51 @@ class VideoPlayerViewModelImpl @Inject constructor(
override val uiState = mutableUiState.asStateFlow() override val uiState = mutableUiState.asStateFlow()
override val events: SharedFlow<VideoPlayerViewModel.Events> = mutableEvent override val events: SharedFlow<VideoPlayerViewModel.Events> = mutableEvent
override var listener: VideoPlayerViewModel.Listener? = null override var listener: VideoPlayerViewModel.Listener? = null
override val player = new_player.player override var newPlayer: NewPlayer? = null
set(value) {
field = value
installExoPlayer()
}
override val player:Player?
get() = newPlayer?.player
private fun installExoPlayer() {
init { player?.let { player ->
player.addListener(object : Player.Listener {
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) {
override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying)
super.onIsPlayingChanged(isPlaying) println("gurken playerstate: $isPlaying")
println("gurken playerstate: $isPlaying") mutableUiState.update {
mutableUiState.update { it.copy(playing = isPlaying)
it.copy(playing = isPlaying)
}
}
// We need to updated the layout of our player view if the video ratio changes
// However, this should be done differently depending on weather we are in
// embedded or fullscreen view.
// If we are in embedded view, we tell the mother layout (only ConstraintLayout supported!)
// to change the ratio of the whole player view.
// If we are in fullscreen we only want to change the ratio of the SurfaceView
override fun onVideoSizeChanged(media3VideoSize: androidx.media3.common.VideoSize) {
super.onVideoSizeChanged(media3VideoSize)
val videoSize = VideoSize.fromMedia3VideoSize(media3VideoSize)
if (current_video_size != videoSize) {
val newRatio = videoSize.getRatio()
if (current_video_size.getRatio() != newRatio) {
mutableUiState.update {
it.copy(contentRatio = newRatio)
}
if (!mutableUiState.value.fullscreen) {
listener?.requestUpdateLayoutRatio(newRatio)
}
} }
current_video_size = videoSize
} }
}
}) // We need to updated the layout of our player view if the video ratio changes
// However, this should be done differently depending on weather we are in
// embedded or fullscreen view.
// If we are in embedded view, we tell the mother layout (only ConstraintLayout supported!)
// to change the ratio of the whole player view.
// If we are in fullscreen we only want to change the ratio of the SurfaceView
override fun onVideoSizeChanged(media3VideoSize: androidx.media3.common.VideoSize) {
super.onVideoSizeChanged(media3VideoSize)
val videoSize = VideoSize.fromMedia3VideoSize(media3VideoSize)
if (current_video_size != videoSize) {
val newRatio = videoSize.getRatio()
if (current_video_size.getRatio() != newRatio) {
mutableUiState.update {
it.copy(contentRatio = newRatio)
}
if (!mutableUiState.value.fullscreen) {
listener?.requestUpdateLayoutRatio(newRatio)
}
}
current_video_size = videoSize
}
}
})
}
} }
@RequiresApi(Build.VERSION_CODES.TIRAMISU) @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@ -161,21 +162,13 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
} }
override fun preparePlayer() {
if (player.playbackState == Player.STATE_IDLE) {
player.prepare()
}
player.setMediaItem(MediaItem.fromUri(app.getString(R.string.portrait_video_example)))
player.playWhenReady = true
}
override fun play() { override fun play() {
player.play() println("gurken player: $newPlayer")
newPlayer?.play()
} }
override fun pause() { override fun pause() {
player.pause() newPlayer?.pause()
} }
override fun prevStream() { override fun prevStream() {
@ -201,8 +194,8 @@ class VideoPlayerViewModelImpl @Inject constructor(
companion object { companion object {
val dummy = object : VideoPlayerViewModel { val dummy = object : VideoPlayerViewModel {
override val new_player = null override var newPlayer: NewPlayer? = null
override val player = null override val player: Player? = null
override val uiState = MutableStateFlow(VideoPlayerUIState.DEFAULT) override val uiState = MutableStateFlow(VideoPlayerUIState.DEFAULT)
override var listener: VideoPlayerViewModel.Listener? = null override var listener: VideoPlayerViewModel.Listener? = null
override val events: SharedFlow<VideoPlayerViewModel.Events>? = null override val events: SharedFlow<VideoPlayerViewModel.Events>? = null
@ -211,10 +204,6 @@ class VideoPlayerViewModelImpl @Inject constructor(
println("dummy impl") println("dummy impl")
} }
override fun preparePlayer() {
println("dummy impl")
}
override fun play() { override fun play() {
println("dummy impl") println("dummy impl")
} }

View File

@ -112,16 +112,13 @@ fun VideoPlayerUI(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
factory = { context -> factory = { context ->
SurfaceView(context).also { view -> SurfaceView(context).also { view ->
println("gurken attach player: ${viewModel.player}")
viewModel.player?.setVideoSurfaceView(view) viewModel.player?.setVideoSurfaceView(view)
} }
}, update = { view -> }, update = { view ->
when (lifecycle) { when (lifecycle) {
Lifecycle.Event.ON_PAUSE -> {
println("gurken state on pause")
}
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {
println("gurken resume") println("gurken reattach player: ${viewModel.player}")
viewModel.player?.setVideoSurfaceView(view) viewModel.player?.setVideoSurfaceView(view)
} }
@ -129,7 +126,7 @@ fun VideoPlayerUI(
} }
}) })
val isPlaying = viewModel.player!!.isPlaying val isPlaying = viewModel.player?.isPlaying ?: false
VideoPlayerControllerUI( VideoPlayerControllerUI(
isPlaying = uiState.playing, isPlaying = uiState.playing,

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/>.
-->
<resources>
<string name="ccc_6502_video">https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4</string>
<string name="ccc_6502_audio">https://ftp.fau.de/cdn.media.ccc.de/congress/2010/ogg-audio-only/27c3-4159-en-reverse_engineering_mos_6502.ogg</string>
<string name="ccc_chromebooks_video">https://ftp.fau.de/cdn.media.ccc.de/congress/2023/h264-hd/37c3-11929-eng-deu-swe-Turning_Chromebooks_into_regular_laptops_hd.mp4</string>
<string name="portrait_video_example">https://videos.pexels.com/video-files/5512609/5512609-hd_1080_1920_25fps.mp4</string>
</resources>

View File

@ -27,18 +27,25 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.VideoPlayerView import net.newpipe.newplayer.VideoPlayerView
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject
lateinit var newPlayer: NewPlayer
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
val video_view = findViewById<VideoPlayerView>(R.id.new_player_video_view) val video_view = findViewById<VideoPlayerView>(R.id.new_player_video_view)
video_view.newPlayer = newPlayer
video_view.minLayoutRatio newPlayer.playWhenReady = true
newPlayer.setStream(getString(R.string.ccc_chromebooks_video))
//TODO: This is a dirty hack. Fix this later on //TODO: This is a dirty hack. Fix this later on
if (getResources().configuration.orientation != Configuration.ORIENTATION_LANDSCAPE) { if (getResources().configuration.orientation != Configuration.ORIENTATION_LANDSCAPE) {

View File

@ -18,11 +18,9 @@
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>. * along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/ */
package net.newpipe.newplayer.internal package net.newpipe.newplayer.testapp
import android.app.Application import android.app.Application
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -32,7 +30,7 @@ import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object VideoPlayerComponent { object NewPlayerComponent {
@Provides @Provides
@Singleton @Singleton
fun provideNewPlayer(app: Application) : NewPlayer { fun provideNewPlayer(app: Application) : NewPlayer {

View File

@ -1,6 +1,27 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- 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/>.
-->
<resources> <resources>
<string name="ccc_6502_video">https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4</string> <string name="ccc_6502_video" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4</string>
<string name="ccc_6502_audio">https://ftp.fau.de/cdn.media.ccc.de/congress/2010/ogg-audio-only/27c3-4159-en-reverse_engineering_mos_6502.ogg</string> <string name="ccc_6502_audio" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2010/ogg-audio-only/27c3-4159-en-reverse_engineering_mos_6502.ogg</string>
<string name="ccc_chromebooks_video">https://ftp.fau.de/cdn.media.ccc.de/congress/2023/h264-hd/37c3-11929-eng-deu-swe-Turning_Chromebooks_into_regular_laptops_hd.mp4</string> <string name="ccc_chromebooks_video" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2023/h264-hd/37c3-11929-eng-deu-swe-Turning_Chromebooks_into_regular_laptops_hd.mp4</string>
<string name="portrait_video_example" translatable="false">https://videos.pexels.com/video-files/5512609/5512609-hd_1080_1920_25fps.mp4</string>
</resources> </resources>