Merge branch 'feature/playbackService' into 'master'
Feature/playback service See merge request agosto182/p2play!15
This commit is contained in:
commit
0705d6dd80
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -379,4 +379,5 @@
|
|||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<attr name="androidBackgroundSecondary" format="reference" />
|
||||
<attr name="androidOnBackgroundSecondary" format="reference" />
|
||||
</resources>
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue