Feature/playback service

This commit is contained in:
Ivan Agosto 2024-04-11 03:15:06 +00:00
parent 48738f100a
commit dd54d214ff
13 changed files with 300 additions and 17 deletions

View File

@ -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"
}

View File

@ -3,6 +3,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application
android:allowBackup="true"
@ -62,6 +64,15 @@
android:exported="false"
android:label="@string/title_activity_settings"
android:theme="@style/Theme.P2play" />
<service
android:name=".services.PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
</application>
</manifest>

View File

@ -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)
}
}
}
}

View File

@ -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()
}

View File

@ -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()

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -379,4 +379,5 @@
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -25,4 +25,16 @@
app:layout_constraintTop_toTopOf="parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<include
android:id="@+id/mini"
layout="@layout/mini_player"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/androidBackgroundSecondary"
android:elevation="5dp"
android:clickable="true"
android:id="@+id/mini_player">
<ImageView
android:id="@+id/mini_player_image"
android:layout_width="wrap_content"
android:layout_height="60dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/default_avatar"
tools:srcCompat="@drawable/default_avatar" />
<TextView
android:id="@+id/mini_player_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:maxWidth="180dp"
android:text="Video"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
app:layout_constraintStart_toEndOf="@+id/mini_player_image"
app:layout_constraintTop_toTopOf="parent"
android:maxLines="1"
android:ellipsize="end"
android:textColor="?attr/androidOnBackgroundSecondary"/>
<TextView
android:id="@+id/mini_player_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginBottom="8dp"
android:maxWidth="180dp"
android:text="Author"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/mini_player_image"
android:maxLines="1"
android:ellipsize="end" />
<ImageView
android:id="@+id/mini_play_pause"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:adjustViewBounds="false"
android:contentDescription="@string/likeBtn"
android:cropToPadding="false"
android:scaleType="center"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_pause_24"
app:tint="@color/colorAccent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -14,6 +14,8 @@
<item name="colorPrimaryDark">@color/colorAccent</item>
<item name="colorAccent">@color/md_theme_dark_onBackground</item>
<item name="android:textColorLink">@color/md_theme_dark_secondary</item>
<item name="androidBackgroundSecondary">@color/md_theme_dark_tertiaryContainer</item>
<item name="androidOnBackgroundSecondary">@color/md_theme_dark_onTertiaryContainer</item>
</style>
<style name="Theme.P2play.NoActionBar" parent="Theme.P2play">

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="androidBackgroundSecondary" format="reference" />
<attr name="androidOnBackgroundSecondary" format="reference" />
</resources>

View File

@ -14,6 +14,8 @@
<item name="colorPrimaryDark">@color/md_theme_light_primary</item>
<item name="colorAccent">@color/md_theme_light_secondary</item>
<item name="android:textColorLink">@color/md_theme_light_secondary</item>
<item name="androidBackgroundSecondary">@color/md_theme_light_tertiaryContainer</item>
<item name="androidOnBackgroundSecondary">@color/md_theme_light_onTertiaryContainer</item>
</style>
<style name="Theme.P2play.NoActionBar" parent="Theme.P2play">