Merge branch 'feature/playbackService' into 'master'

Feature/playback service

See merge request agosto182/p2play!15
This commit is contained in:
Ivan Agosto 2024-04-11 03:15:06 +00:00
commit 0705d6dd80
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-exoplayer-dash:1.1.1'
implementation 'androidx.media3:media3-ui:1.1.1' implementation 'androidx.media3:media3-ui:1.1.1'
implementation 'androidx.media3:media3-exoplayer-hls: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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <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 <application
android:allowBackup="true" android:allowBackup="true"
@ -62,6 +64,15 @@
android:exported="false" android:exported="false"
android:label="@string/title_activity_settings" android:label="@string/title_activity_settings"
android:theme="@style/Theme.P2play" /> 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> </application>
</manifest> </manifest>

View File

@ -6,22 +6,32 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.transition.Visibility
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.activity_main.drawer_layout import kotlinx.android.synthetic.main.activity_main.drawer_layout
import kotlinx.android.synthetic.main.activity_main.nav_view import kotlinx.android.synthetic.main.activity_main.nav_view
import kotlinx.android.synthetic.main.app_bar_main.toolbar 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.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.nav_header_main.*
import kotlinx.android.synthetic.main.view_video.view.thumb
import org.libre.agosto.p2play.adapters.VideosAdapter import org.libre.agosto.p2play.adapters.VideosAdapter
import org.libre.agosto.p2play.ajax.Videos import org.libre.agosto.p2play.ajax.Videos
import org.libre.agosto.p2play.models.VideoModel import org.libre.agosto.p2play.models.VideoModel
import org.libre.agosto.p2play.singletons.PlaybackSingleton
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
@ -34,7 +44,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
var section: String = "" var section: String = ""
var searchVal: String = "" var searchVal: String = ""
var pagination = 0 var pagination = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
@ -62,6 +71,12 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
this.refresh() 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({ Handler().postDelayed({
// Title for nav_bar // Title for nav_bar
side_emailTxt?.text = getString(R.string.nav_header_subtitle) + " " + this.packageManager.getPackageInfo(this.packageName, 0).versionName 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() { override fun onResume() {
super.onResume() super.onResume()
setSideData() 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() { 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.content.pm.ActivityInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri
import android.os.AsyncTask import android.os.AsyncTask
import android.os.Bundle import android.os.Bundle
import android.os.Looper import android.os.Looper
@ -18,9 +19,11 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.upstream.DefaultAllocator import androidx.media3.exoplayer.upstream.DefaultAllocator
import androidx.media3.session.MediaController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Picasso 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.helpers.setFullscreen
import org.libre.agosto.p2play.models.CommentaryModel import org.libre.agosto.p2play.models.CommentaryModel
import org.libre.agosto.p2play.models.VideoModel import org.libre.agosto.p2play.models.VideoModel
import org.libre.agosto.p2play.singletons.PlaybackSingleton
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
class ReproductorActivity : AppCompatActivity() { class ReproductorActivity : AppCompatActivity() {
private val clientVideo: Videos = Videos() private val clientVideo: Videos = Videos()
lateinit var video: VideoModel lateinit var video: VideoModel
lateinit var videoPlayback: VideoModel
private val actions: Actions = Actions() private val actions: Actions = Actions()
private val client: Comments = Comments() private val client: Comments = Comments()
private val videos: Videos = Videos() private val videos: Videos = Videos()
@ -49,9 +54,14 @@ class ReproductorActivity : AppCompatActivity() {
// Exoplayer // Exoplayer
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer
private lateinit var mediaControl: MediaController
// Fullscreen info // Fullscreen info
private var isFullscreen = false private var isFullscreen = false
// Resume info
private var isResume = false
@SuppressLint("SetJavaScriptEnabled", "SetTextI18n") @SuppressLint("SetJavaScriptEnabled", "SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -70,7 +80,15 @@ class ReproductorActivity : AppCompatActivity() {
videoView.settings.domStorageEnabled = true videoView.settings.domStorageEnabled = true
try { try {
val resume = this.intent.extras?.getSerializable("resume")
if (resume == null) {
video = this.intent.extras?.getSerializable("video") as VideoModel video = this.intent.extras?.getSerializable("video") as VideoModel
isResume = false
} else {
video = PlaybackSingleton.video!!
isResume = true
}
tittleVideoTxt.text = this.video.name tittleVideoTxt.text = this.video.name
viewsTxt.text = "${this.video.views} ${getString(R.string.view_text)}" viewsTxt.text = "${this.video.views} ${getString(R.string.view_text)}"
userTxt.text = this.video.username userTxt.text = this.video.username
@ -113,7 +131,7 @@ class ReproductorActivity : AppCompatActivity() {
} }
AsyncTask.execute { AsyncTask.execute {
val video = this.clientVideo.getVideo(this.video.uuid) videoPlayback = this.clientVideo.getVideo(this.video.uuid)
// TODO: Make this configurable // TODO: Make this configurable
val bufferSize = 1024 * 1024 // 1mb val bufferSize = 1024 * 1024 // 1mb
val allocator = DefaultAllocator(true, bufferSize) val allocator = DefaultAllocator(true, bufferSize)
@ -123,19 +141,30 @@ class ReproductorActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
try { try {
player = ExoPlayer.Builder(this.baseContext) if (PlaybackSingleton.player == null || !PlaybackSingleton.player!!.playWhenReady) {
PlaybackSingleton.player = ExoPlayer.Builder(this.baseContext)
.setSeekBackIncrementMs(10000) .setSeekBackIncrementMs(10000)
.setSeekForwardIncrementMs(10000) .setSeekForwardIncrementMs(10000)
.setLoadControl(loadControl).build() .setLoadControl(loadControl).build()
}
player = PlaybackSingleton.player!!
exoPlayer.player = player exoPlayer.player = player
println("----- video --------") println("----- video --------")
println(video.streamingData?.playlistUrl) println(videoPlayback.streamingData?.playlistUrl)
val mediaItem = MediaItem.fromUri(video.streamingData?.playlistUrl!!)
// Set the media item to be played. if (!isResume) {
player.setMediaItem(mediaItem) val mediaItem = MediaItem.Builder()
// Prepare the player. .setUri(videoPlayback.streamingData?.playlistUrl!!)
player.prepare() .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. // Start the playback.
// player.play() // player.play()
} catch (err: Exception) { } catch (err: Exception) {
@ -392,7 +421,9 @@ class ReproductorActivity : AppCompatActivity() {
} }
override fun onDestroy() { override fun onDestroy() {
player.release() if (!player.isPlaying) {
PlaybackSingleton.release()
}
super.onDestroy() super.onDestroy()
} }

View File

@ -71,14 +71,14 @@ class VideoModel(
data.endArray() data.endArray()
} }
"files" -> { "files" -> {
if (streamingData === null) {
data.beginArray() data.beginArray()
if (streamingData === null) {
if (data.hasNext()) { if (data.hasNext()) {
data.beginObject() data.beginObject()
while (data.hasNext()) { while (data.hasNext()) {
val key2 = data.nextName() val key2 = data.nextName()
when (key2.toString()) { when (key2.toString()) {
"fileDownloadUrl" -> { "fileUrl" -> {
streamingData = StreamingModel() streamingData = StreamingModel()
streamingData!!.playlistUrl = data.nextString() streamingData!!.playlistUrl = data.nextString()
} }
@ -87,9 +87,12 @@ class VideoModel(
} }
data.endObject() data.endObject()
} }
data.endArray() while (data.hasNext()) {
data.skipValue()
} }
} }
data.endArray()
}
"channel" -> { "channel" -> {
data.beginObject() data.beginObject()
while (data.hasNext()) { while (data.hasNext()) {

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> </LinearLayout>
</ScrollView> </ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -25,4 +25,16 @@
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </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> </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="colorPrimaryDark">@color/colorAccent</item>
<item name="colorAccent">@color/md_theme_dark_onBackground</item> <item name="colorAccent">@color/md_theme_dark_onBackground</item>
<item name="android:textColorLink">@color/md_theme_dark_secondary</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>
<style name="Theme.P2play.NoActionBar" parent="Theme.P2play"> <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="colorPrimaryDark">@color/md_theme_light_primary</item>
<item name="colorAccent">@color/md_theme_light_secondary</item> <item name="colorAccent">@color/md_theme_light_secondary</item>
<item name="android:textColorLink">@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>
<style name="Theme.P2play.NoActionBar" parent="Theme.P2play"> <style name="Theme.P2play.NoActionBar" parent="Theme.P2play">