From dd54d214ff7663202052cf34c873730f47945626 Mon Sep 17 00:00:00 2001 From: Ivan Agosto Date: Thu, 11 Apr 2024 03:15:06 +0000 Subject: [PATCH] Feature/playback service --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 11 ++++ .../org/libre/agosto/p2play/MainActivity.kt | 52 ++++++++++++++- .../agosto/p2play/ReproductorActivity.kt | 57 ++++++++++++---- .../libre/agosto/p2play/models/VideoModel.kt | 9 ++- .../agosto/p2play/services/PlaybackService.kt | 47 +++++++++++++ .../p2play/singletons/PlaybackSingleton.kt | 52 +++++++++++++++ .../main/res/layout/activity_reproductor.xml | 1 + app/src/main/res/layout/content_main.xml | 12 ++++ app/src/main/res/layout/mini_player.xml | 66 +++++++++++++++++++ app/src/main/res/values-night/themes.xml | 2 + app/src/main/res/values/attrs.xml | 5 ++ app/src/main/res/values/themes.xml | 2 + 13 files changed, 300 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/libre/agosto/p2play/services/PlaybackService.kt create mode 100644 app/src/main/java/org/libre/agosto/p2play/singletons/PlaybackSingleton.kt create mode 100644 app/src/main/res/layout/mini_player.xml create mode 100644 app/src/main/res/values/attrs.xml diff --git a/app/build.gradle b/app/build.gradle index adbf60d..96a8791 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,4 +50,5 @@ dependencies { implementation 'androidx.media3:media3-exoplayer-dash:1.1.1' implementation 'androidx.media3:media3-ui:1.1.1' implementation 'androidx.media3:media3-exoplayer-hls:1.1.1' + implementation "androidx.media3:media3-session:1.1.1" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2371a50..7721137 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/libre/agosto/p2play/MainActivity.kt b/app/src/main/java/org/libre/agosto/p2play/MainActivity.kt index b804471..ba68b37 100644 --- a/app/src/main/java/org/libre/agosto/p2play/MainActivity.kt +++ b/app/src/main/java/org/libre/agosto/p2play/MainActivity.kt @@ -6,22 +6,32 @@ import android.os.Bundle import android.os.Handler import android.view.Menu import android.view.MenuItem +import android.view.View import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.core.view.GravityCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.transition.Visibility import com.google.android.material.navigation.NavigationView import com.squareup.picasso.Picasso import kotlinx.android.synthetic.main.activity_main.drawer_layout import kotlinx.android.synthetic.main.activity_main.nav_view import kotlinx.android.synthetic.main.app_bar_main.toolbar +import kotlinx.android.synthetic.main.content_main.mini import kotlinx.android.synthetic.main.content_main.swipeContainer +import kotlinx.android.synthetic.main.mini_player.mini_play_pause +import kotlinx.android.synthetic.main.mini_player.mini_player +import kotlinx.android.synthetic.main.mini_player.mini_player_author +import kotlinx.android.synthetic.main.mini_player.mini_player_image +import kotlinx.android.synthetic.main.mini_player.mini_player_title import kotlinx.android.synthetic.main.nav_header_main.* +import kotlinx.android.synthetic.main.view_video.view.thumb import org.libre.agosto.p2play.adapters.VideosAdapter import org.libre.agosto.p2play.ajax.Videos import org.libre.agosto.p2play.models.VideoModel +import org.libre.agosto.p2play.singletons.PlaybackSingleton class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { private lateinit var recyclerView: RecyclerView @@ -34,7 +44,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte var section: String = "" var searchVal: String = "" var pagination = 0 - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -62,6 +71,12 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte this.refresh() } + mini_player_image.setOnClickListener { this.resumeVideo() } + mini_player_title.setOnClickListener { this.resumeVideo() } + mini_player_author.setOnClickListener { this.resumeVideo() } + mini.setOnClickListener { this.resumeVideo() } + mini_play_pause.setOnClickListener { this.playPausePlayer() } + Handler().postDelayed({ // Title for nav_bar side_emailTxt?.text = getString(R.string.nav_header_subtitle) + " " + this.packageManager.getPackageInfo(this.packageName, 0).versionName @@ -347,6 +362,23 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte override fun onResume() { super.onResume() setSideData() + + if (PlaybackSingleton.player != null && PlaybackSingleton.player!!.isPlaying) { + PlaybackSingleton.runMediaSession(this) + mini_player_title.text = PlaybackSingleton.video!!.name + mini_player_author.text = PlaybackSingleton.video!!.username + Picasso.get().load("https://${ManagerSingleton.url}${PlaybackSingleton.video!!.thumbUrl}").into(mini_player_image) + mini.visibility = View.VISIBLE + } else { + mini.visibility = View.GONE + } + } + + override fun onDestroy() { + if (PlaybackSingleton.player != null) { + PlaybackSingleton.release() + } + super.onDestroy() } private fun setSideData() { @@ -423,4 +455,22 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte } } } + + private fun resumeVideo () { + val intent = Intent(this, ReproductorActivity::class.java) + intent.putExtra("resume", true) + startActivity(intent) + } + + private fun playPausePlayer () { + PlaybackSingleton.player?.let { + if (it.isPlaying) { + it.pause() + mini_play_pause.setImageResource(R.drawable.ic_play_arrow_24) + } else { + it.play() + mini_play_pause.setImageResource(R.drawable.ic_pause_24) + } + } + } } diff --git a/app/src/main/java/org/libre/agosto/p2play/ReproductorActivity.kt b/app/src/main/java/org/libre/agosto/p2play/ReproductorActivity.kt index 41fb856..1a45138 100644 --- a/app/src/main/java/org/libre/agosto/p2play/ReproductorActivity.kt +++ b/app/src/main/java/org/libre/agosto/p2play/ReproductorActivity.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.content.pm.ActivityInfo import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.net.Uri import android.os.AsyncTask import android.os.Bundle import android.os.Looper @@ -18,9 +19,11 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.upstream.DefaultAllocator +import androidx.media3.session.MediaController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.squareup.picasso.Picasso @@ -32,11 +35,13 @@ import org.libre.agosto.p2play.ajax.Videos import org.libre.agosto.p2play.helpers.setFullscreen import org.libre.agosto.p2play.models.CommentaryModel import org.libre.agosto.p2play.models.VideoModel +import org.libre.agosto.p2play.singletons.PlaybackSingleton @Suppress("NAME_SHADOWING") class ReproductorActivity : AppCompatActivity() { private val clientVideo: Videos = Videos() lateinit var video: VideoModel + lateinit var videoPlayback: VideoModel private val actions: Actions = Actions() private val client: Comments = Comments() private val videos: Videos = Videos() @@ -49,9 +54,14 @@ class ReproductorActivity : AppCompatActivity() { // Exoplayer private lateinit var player: ExoPlayer + private lateinit var mediaControl: MediaController + // Fullscreen info private var isFullscreen = false + // Resume info + private var isResume = false + @SuppressLint("SetJavaScriptEnabled", "SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -70,7 +80,15 @@ class ReproductorActivity : AppCompatActivity() { videoView.settings.domStorageEnabled = true try { - video = this.intent.extras?.getSerializable("video") as VideoModel + val resume = this.intent.extras?.getSerializable("resume") + if (resume == null) { + video = this.intent.extras?.getSerializable("video") as VideoModel + isResume = false + } else { + video = PlaybackSingleton.video!! + isResume = true + } + tittleVideoTxt.text = this.video.name viewsTxt.text = "${this.video.views} ${getString(R.string.view_text)}" userTxt.text = this.video.username @@ -113,7 +131,7 @@ class ReproductorActivity : AppCompatActivity() { } AsyncTask.execute { - val video = this.clientVideo.getVideo(this.video.uuid) + videoPlayback = this.clientVideo.getVideo(this.video.uuid) // TODO: Make this configurable val bufferSize = 1024 * 1024 // 1mb val allocator = DefaultAllocator(true, bufferSize) @@ -123,19 +141,30 @@ class ReproductorActivity : AppCompatActivity() { runOnUiThread { try { - player = ExoPlayer.Builder(this.baseContext) - .setSeekBackIncrementMs(10000) - .setSeekForwardIncrementMs(10000) - .setLoadControl(loadControl).build() + if (PlaybackSingleton.player == null || !PlaybackSingleton.player!!.playWhenReady) { + PlaybackSingleton.player = ExoPlayer.Builder(this.baseContext) + .setSeekBackIncrementMs(10000) + .setSeekForwardIncrementMs(10000) + .setLoadControl(loadControl).build() + } + player = PlaybackSingleton.player!! exoPlayer.player = player println("----- video --------") - println(video.streamingData?.playlistUrl) - val mediaItem = MediaItem.fromUri(video.streamingData?.playlistUrl!!) - // Set the media item to be played. - player.setMediaItem(mediaItem) - // Prepare the player. - player.prepare() + println(videoPlayback.streamingData?.playlistUrl) + + if (!isResume) { + val mediaItem = MediaItem.Builder() + .setUri(videoPlayback.streamingData?.playlistUrl!!) + .setMediaMetadata( + MediaMetadata.Builder() + .setArtist(videoPlayback.username) + .setTitle(videoPlayback.name) + .setArtworkUri(Uri.parse("https://${ManagerSingleton.url}${videoPlayback.thumbUrl}")) + .build(), + ).build() + PlaybackSingleton.setData(mediaItem, video) + } // Start the playback. // player.play() } catch (err: Exception) { @@ -392,7 +421,9 @@ class ReproductorActivity : AppCompatActivity() { } override fun onDestroy() { - player.release() + if (!player.isPlaying) { + PlaybackSingleton.release() + } super.onDestroy() } diff --git a/app/src/main/java/org/libre/agosto/p2play/models/VideoModel.kt b/app/src/main/java/org/libre/agosto/p2play/models/VideoModel.kt index 5955616..8bc594e 100644 --- a/app/src/main/java/org/libre/agosto/p2play/models/VideoModel.kt +++ b/app/src/main/java/org/libre/agosto/p2play/models/VideoModel.kt @@ -71,14 +71,14 @@ class VideoModel( data.endArray() } "files" -> { + data.beginArray() if (streamingData === null) { - data.beginArray() if (data.hasNext()) { data.beginObject() while (data.hasNext()) { val key2 = data.nextName() when (key2.toString()) { - "fileDownloadUrl" -> { + "fileUrl" -> { streamingData = StreamingModel() streamingData!!.playlistUrl = data.nextString() } @@ -87,8 +87,11 @@ class VideoModel( } data.endObject() } - data.endArray() + while (data.hasNext()) { + data.skipValue() + } } + data.endArray() } "channel" -> { data.beginObject() diff --git a/app/src/main/java/org/libre/agosto/p2play/services/PlaybackService.kt b/app/src/main/java/org/libre/agosto/p2play/services/PlaybackService.kt new file mode 100644 index 0000000..bc13c2e --- /dev/null +++ b/app/src/main/java/org/libre/agosto/p2play/services/PlaybackService.kt @@ -0,0 +1,47 @@ +package org.libre.agosto.p2play.services + +import android.app.PendingIntent +import android.content.Intent +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSessionService +import org.libre.agosto.p2play.ReproductorActivity +import org.libre.agosto.p2play.singletons.PlaybackSingleton + +class PlaybackService : MediaSessionService() { + private var mediaSession: MediaSession? = null + + // Create your Player and MediaSession in the onCreate lifecycle event + override fun onCreate() { + super.onCreate() + val player = PlaybackSingleton.player!! + mediaSession = MediaSession.Builder(this, player) + .build() + val contentIntent = Intent(this, ReproductorActivity::class.java) + contentIntent.putExtra("resume", true) + val pendingIntent = PendingIntent.getActivity( + this, + 0, + contentIntent, + PendingIntent.FLAG_MUTABLE, + ) + mediaSession!!.setSessionActivity(pendingIntent) + } + + // Remember to release the player and media session in onDestroy + override fun onDestroy() { + mediaSession?.run { + release() + mediaSession = null + } + super.onDestroy() + } + + override fun onTaskRemoved(rootIntent: Intent?) { + this.mediaSession!!.player.stop() + super.onTaskRemoved(rootIntent) + } + + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { + return mediaSession + } +} diff --git a/app/src/main/java/org/libre/agosto/p2play/singletons/PlaybackSingleton.kt b/app/src/main/java/org/libre/agosto/p2play/singletons/PlaybackSingleton.kt new file mode 100644 index 0000000..c63537c --- /dev/null +++ b/app/src/main/java/org/libre/agosto/p2play/singletons/PlaybackSingleton.kt @@ -0,0 +1,52 @@ +package org.libre.agosto.p2play.singletons + +import android.content.ComponentName +import android.content.Context +import android.util.Log +import androidx.media3.common.MediaItem +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.session.MediaController +import androidx.media3.session.SessionToken +import com.google.common.util.concurrent.MoreExecutors +import org.libre.agosto.p2play.models.VideoModel +import org.libre.agosto.p2play.services.PlaybackService + +object PlaybackSingleton { + var player: ExoPlayer? = null + var video: VideoModel? = null + private var withMediaSession = false + + fun setData(mediaItem: MediaItem, video: VideoModel): ExoPlayer? { + player?.let { + if (it.isPlaying) { + it.pause() + } + it.setMediaItem(mediaItem) + it.prepare() + this.video = video + return it + } + + return null + } + + fun release() { + player?.release() + } + + fun runMediaSession(context: Context) { + if (!this.withMediaSession) { + val sessionToken = SessionToken(context, ComponentName(context, PlaybackService::class.java)) + + val controllerFuture = MediaController.Builder(context, sessionToken).buildAsync() + + controllerFuture.addListener( + { + val med = controllerFuture.get() + }, + MoreExecutors.directExecutor(), + ) + this.withMediaSession = true + } + } +} diff --git a/app/src/main/res/layout/activity_reproductor.xml b/app/src/main/res/layout/activity_reproductor.xml index 892d06c..38b99c8 100644 --- a/app/src/main/res/layout/activity_reproductor.xml +++ b/app/src/main/res/layout/activity_reproductor.xml @@ -379,4 +379,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 4577734..0ccecaf 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -25,4 +25,16 @@ app:layout_constraintTop_toTopOf="parent" /> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/mini_player.xml b/app/src/main/res/layout/mini_player.xml new file mode 100644 index 0000000..ca3bc6f --- /dev/null +++ b/app/src/main/res/layout/mini_player.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 5246f13..0f3c88f 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -14,6 +14,8 @@ @color/colorAccent @color/md_theme_dark_onBackground @color/md_theme_dark_secondary + @color/md_theme_dark_tertiaryContainer + @color/md_theme_dark_onTertiaryContainer