Merge pull request #990 from Bnyro/notification

Notification and autoplay refactor
This commit is contained in:
Bnyro 2022-08-07 20:02:36 +02:00 committed by GitHub
commit 79de2ab9d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 283 additions and 279 deletions

View File

@ -1,7 +1,6 @@
package com.github.libretube.fragments
import android.app.ActivityManager
import android.app.NotificationManager
import android.app.PictureInPictureParams
import android.content.Context
import android.content.Intent
@ -16,7 +15,6 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat
import android.text.Html
import android.text.format.DateUtils
import android.util.Log
@ -35,9 +33,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.BACKGROUND_CHANNEL_ID
import com.github.libretube.Globals
import com.github.libretube.PLAYER_NOTIFICATION_ID
import com.github.libretube.R
import com.github.libretube.activities.MainActivity
import com.github.libretube.adapters.ChaptersAdapter
@ -50,17 +46,17 @@ import com.github.libretube.dialogs.AddToPlaylistDialog
import com.github.libretube.dialogs.DownloadDialog
import com.github.libretube.dialogs.ShareDialog
import com.github.libretube.obj.ChapterSegment
import com.github.libretube.obj.Playlist
import com.github.libretube.obj.Segment
import com.github.libretube.obj.Segments
import com.github.libretube.obj.Streams
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.services.BackgroundMode
import com.github.libretube.util.AutoPlayHelper
import com.github.libretube.util.BackgroundHelper
import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.CronetHelper
import com.github.libretube.util.DescriptionAdapter
import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.OnDoubleTapEventListener
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.RetrofitInstance
@ -77,7 +73,6 @@ import com.google.android.exoplayer2.MediaItem.fromUri
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.cronet.CronetDataSource
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.MergingMediaSource
@ -85,7 +80,6 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.CaptionStyleCompat
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.upstream.DataSource
@ -175,15 +169,12 @@ class PlayerFragment : Fragment() {
* for autoplay
*/
private var nextStreamId: String? = null
private var playlistStreamIds: MutableList<String> = arrayListOf()
private var playlistNextPage: String? = null
private lateinit var autoPlayHelper: AutoPlayHelper
/**
* for the player notification
*/
private lateinit var mediaSession: MediaSessionCompat
private lateinit var mediaSessionConnector: MediaSessionConnector
private lateinit var playerNotification: PlayerNotificationManager
private lateinit var nowPlayingNotification: NowPlayingNotification
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -676,15 +667,7 @@ class PlayerFragment : Fragment() {
super.onDestroy()
try {
saveWatchPosition()
mediaSession.isActive = false
mediaSession.release()
mediaSessionConnector.setPlayer(null)
playerNotification.setPlayer(null)
val notificationManager = context?.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.cancel(1)
exoPlayer.release()
nowPlayingNotification.destroy()
activity?.requestedOrientation =
if ((activity as MainActivity).autoRotationEnabled) ActivityInfo.SCREEN_ORIENTATION_USER
else ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
@ -752,12 +735,12 @@ class PlayerFragment : Fragment() {
exoPlayer.prepare()
exoPlayer.play()
exoPlayerView.useController = true
initializePlayerNotification(requireContext())
initializePlayerNotification()
if (sponsorBlockEnabled) fetchSponsorBlockSegments()
// show comments if related streams disabled
if (!relatedStreamsEnabled) toggleComments()
// prepare for autoplay
initAutoPlay()
if (autoplayEnabled) setNextStream()
if (watchHistoryEnabled) {
PreferenceHelper.addToWatchHistory(videoId!!, streams)
}
@ -767,6 +750,20 @@ class PlayerFragment : Fragment() {
run()
}
/**
* set the videoId of the next stream for autoplay
*/
private fun setNextStream() {
nextStreamId = streams.relatedStreams!![0].url.toID()
if (playlistId == null) return
if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId!!)
// search for the next videoId in the playlist
lifecycleScope.launchWhenCreated {
val nextId = autoPlayHelper.getNextPlaylistVideoId(videoId!!)
if (nextId != null) nextStreamId = nextId
}
}
/**
* fetch the segments for SponsorBlock
*/
@ -824,59 +821,6 @@ class PlayerFragment : Fragment() {
if (position != null) exoPlayer.seekTo(position!!)
}
// the function is working recursively
private fun initAutoPlay() {
// save related streams for autoplay
if (autoplayEnabled) {
// if it's a playlist use the next video
if (playlistId != null) {
lateinit var playlist: Playlist // var for saving the list in
// runs only the first time when starting a video from a playlist
if (playlistStreamIds.isEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
// fetch the playlists videos
playlist = RetrofitInstance.api.getPlaylist(playlistId!!)
// save the playlist urls in the array
playlist.relatedStreams?.forEach { video ->
playlistStreamIds += video.url.toID()
}
// save playlistNextPage for usage if video is not contained
playlistNextPage = playlist.nextpage
// restart the function after videos are loaded
initAutoPlay()
}
}
// if the playlists contain the video, then save the next video as next stream
else if (playlistStreamIds.contains(videoId)) {
val index = playlistStreamIds.indexOf(videoId)
// check whether there's a next video
if (index + 1 <= playlistStreamIds.size) {
nextStreamId = playlistStreamIds[index + 1]
}
// fetch the next page of the playlist if the video isn't contained
} else if (playlistNextPage != null) {
CoroutineScope(Dispatchers.IO).launch {
RetrofitInstance.api.getPlaylistNextPage(playlistId!!, playlistNextPage!!)
// append all the playlist item urls to the array
playlist.relatedStreams?.forEach { video ->
playlistStreamIds += video.url.toID()
}
// save playlistNextPage for usage if video is not contained
playlistNextPage = playlist.nextpage
// restart the function after videos are loaded
initAutoPlay()
}
}
// else: the video must be the last video of the playlist so nothing happens
// if it's not a playlist then use the next related video
} else if (streams.relatedStreams != null && streams.relatedStreams!!.isNotEmpty()) {
// save next video from related streams for autoplay
nextStreamId = streams.relatedStreams!![0].url.toID()
}
}
}
// used for autoplay and skipping to next video
private fun playNextVideo() {
// check whether there is a new video in the queue
@ -1502,33 +1446,14 @@ class PlayerFragment : Fragment() {
exoPlayer.setAudioAttributes(audioAttributes, true)
}
private fun initializePlayerNotification(c: Context) {
mediaSession = MediaSessionCompat(c, this.javaClass.name)
mediaSession.apply {
isActive = true
}
mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setPlayer(exoPlayer)
playerNotification = PlayerNotificationManager
.Builder(c, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID)
.setMediaDescriptionAdapter(
DescriptionAdapter(
streams.title!!,
streams.uploader!!,
streams.thumbnailUrl!!,
requireContext()
)
)
.build()
playerNotification.apply {
setPlayer(exoPlayer)
setUsePreviousAction(false)
setUseStopAction(true)
setMediaSessionToken(mediaSession.sessionToken)
/**
* show the [NowPlayingNotification] for the current video
*/
private fun initializePlayerNotification() {
if (!this::nowPlayingNotification.isInitialized) {
nowPlayingNotification = NowPlayingNotification(requireContext(), exoPlayer)
}
nowPlayingNotification.updatePlayerNotification(streams)
}
// lock the player

View File

@ -4,13 +4,11 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.support.v4.media.session.MediaSessionCompat
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.BACKGROUND_CHANNEL_ID
import com.github.libretube.PLAYER_NOTIFICATION_ID
@ -20,7 +18,7 @@ import com.github.libretube.obj.Segments
import com.github.libretube.obj.Streams
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.DescriptionAdapter
import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.toID
@ -30,7 +28,6 @@ import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -56,21 +53,6 @@ class BackgroundMode : Service() {
private var player: ExoPlayer? = null
private var playWhenReadyPlayer = true
/**
* The [MediaSessionCompat] for the [response].
*/
private lateinit var mediaSession: MediaSessionCompat
/**
* The [MediaSessionConnector] to connect with the [mediaSession] and implement it with the [player].
*/
private lateinit var mediaSessionConnector: MediaSessionConnector
/**
* The [PlayerNotificationManager] to load the [mediaSession] content on it.
*/
private var playerNotification: PlayerNotificationManager? = null
/**
* The [AudioAttributes] handle the audio focus of the [player]
*/
@ -81,11 +63,16 @@ class BackgroundMode : Service() {
*/
private var segmentData: Segments? = null
/**
* Notification for the player
*/
private lateinit var nowPlayingNotification: NowPlayingNotification
/**
* Setting the required [notification] for running as a foreground service
*/
override fun onCreate() {
super.onCreate()
/**
* setting the required notification for running as a foreground service
*/
if (Build.VERSION.SDK_INT >= 26) {
val channelId = BACKGROUND_CHANNEL_ID
val channel = NotificationChannel(
@ -106,15 +93,17 @@ class BackgroundMode : Service() {
* Initializes the [player] with the [MediaItem].
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// destroy the old player
destroyPlayer()
try {
// get the intent arguments
videoId = intent?.getStringExtra("videoId")!!
val position = intent.getLongExtra("position", 0L)
// get the intent arguments
videoId = intent?.getStringExtra("videoId")!!
val position = intent.getLongExtra("position", 0L)
// play the audio in the background
playAudio(videoId, position)
// play the audio in the background
playAudio(videoId, position)
} catch (e: Exception) {
stopForeground(true)
stopSelf()
}
return super.onStartCommand(intent, flags, startId)
}
@ -133,7 +122,13 @@ class BackgroundMode : Service() {
job.join()
initializePlayer()
initializePlayerNotification()
setMediaItem()
// create the notification
if (!this@BackgroundMode::nowPlayingNotification.isInitialized) {
nowPlayingNotification = NowPlayingNotification(this@BackgroundMode, player!!)
}
nowPlayingNotification.updatePlayerNotification(response!!)
player?.apply {
playWhenReady = playWhenReadyPlayer
@ -174,16 +169,11 @@ class BackgroundMode : Service() {
if (autoplay) playNextVideo()
}
Player.STATE_IDLE -> {
// called when the user pressed stop in the notification
// stop the service from being in the foreground and remove the notification
stopForeground(true)
// destroy the service
stopSelf()
onDestroy()
}
}
}
})
setMediaItem()
}
/**
@ -194,9 +184,6 @@ class BackgroundMode : Service() {
val videoId = response!!
.relatedStreams!![0].url.toID()
// destroy previous notification and player
destroyPlayer()
// play new video on background
this.videoId = videoId
this.segmentData = null
@ -204,32 +191,6 @@ class BackgroundMode : Service() {
}
}
/**
* Initializes the [playerNotification] attached to the [player] and shows it.
*/
private fun initializePlayerNotification() {
playerNotification = PlayerNotificationManager
.Builder(this, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID)
// set the description of the notification
.setMediaDescriptionAdapter(
DescriptionAdapter(
response?.title!!,
response?.uploader!!,
response?.thumbnailUrl!!,
this
)
)
.build()
playerNotification?.apply {
setPlayer(player)
setUseNextAction(false)
setUsePreviousAction(false)
setUseStopAction(true)
setColorized(true)
setMediaSessionToken(mediaSession.sessionToken)
}
}
/**
* Sets the [MediaItem] with the [response] into the [player]. Also creates a [MediaSessionConnector]
* with the [mediaSession] and attach it to the [player].
@ -239,12 +200,6 @@ class BackgroundMode : Service() {
val mediaItem = MediaItem.Builder().setUri(it.hls!!).build()
player?.setMediaItem(mediaItem)
}
mediaSession = MediaSessionCompat(this, this.javaClass.name)
mediaSession.isActive = true
mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setPlayer(player)
}
/**
@ -284,15 +239,17 @@ class BackgroundMode : Service() {
}
}
private fun destroyPlayer() {
// clear old player and its notification
playerNotification = null
player = null
// kill old notification
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
notificationManager.cancel(PLAYER_NOTIFICATION_ID)
/**
* destroy the [BackgroundMode] foreground service
*/
override fun onDestroy() {
// called when the user pressed stop in the notification
// stop the service from being in the foreground and remove the notification
stopForeground(true)
// destroy the service
stopSelf()
if (this::nowPlayingNotification.isInitialized) nowPlayingNotification.destroy()
super.onDestroy()
}
override fun onBind(p0: Intent?): IBinder? {

View File

@ -0,0 +1,37 @@
package com.github.libretube.util
import com.github.libretube.obj.Playlist
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AutoPlayHelper(
private val playlistId: String
) {
private val TAG = "AutoPlayHelper"
private val playlistStreamIds = mutableListOf<String>()
private lateinit var playlist: Playlist
private var playlistNextPage: String? = null
suspend fun getNextPlaylistVideoId(currentVideoId: String): String? {
// if the playlists contain the video, then save the next video as next stream
if (playlistStreamIds.contains(currentVideoId)) {
val index = playlistStreamIds.indexOf(currentVideoId)
// check whether there's a next video
return if (index < playlistStreamIds.size) playlistStreamIds[index + 1]
else getNextPlaylistVideoId(currentVideoId)
} else if (playlistStreamIds.isEmpty() || playlistNextPage != null) {
// fetch the next page of the playlist
return withContext(Dispatchers.IO) {
// fetch the playlists videos
playlist = RetrofitInstance.api.getPlaylist(playlistId)
// save the playlist urls in the array
playlistStreamIds += playlist.relatedStreams!!.map { it.url.toID() }
// save playlistNextPage for usage if video is not contained
playlistNextPage = playlist.nextpage
return@withContext getNextPlaylistVideoId(currentVideoId)
}
}
return null
}
}

View File

@ -1,95 +0,0 @@
package com.github.libretube.util
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.github.libretube.activities.MainActivity
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import java.net.URL
/**
* The [DescriptionAdapter] is used to show title, uploaderName and thumbnail of the video in the notification
* Basic example [here](https://github.com/AnthonyMarkD/AudioPlayerSampleTest)
*/
class DescriptionAdapter(
private val title: String,
private val channelName: String,
private val thumbnailUrl: String,
private val context: Context
) :
PlayerNotificationManager.MediaDescriptionAdapter {
/**
* sets the title of the notification
*/
override fun getCurrentContentTitle(player: Player): CharSequence {
// return controller.metadata.description.title.toString()
return title
}
/**
* overrides the action when clicking the notification
*/
override fun createCurrentContentIntent(player: Player): PendingIntent? {
// return controller.sessionActivity
/**
* starts a new MainActivity Intent when the player notification is clicked
* it doesn't start a completely new MainActivity because the MainActivity's launchMode
* is set to "singleTop" in the AndroidManifest (important!!!)
* that's the only way to launch back into the previous activity (e.g. the player view
*/
val intent = Intent(context, MainActivity::class.java)
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
}
/**
* the description of the notification (below the title)
*/
override fun getCurrentContentText(player: Player): CharSequence? {
// return controller.metadata.description.subtitle.toString()
return channelName
}
/**
* return the icon/thumbnail of the video
*/
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
lateinit var bitmap: Bitmap
/**
* running on a new thread to prevent a NetworkMainThreadException
*/
val thread = Thread {
try {
/**
* try to GET the thumbnail from the URL
*/
val inputStream = URL(thumbnailUrl).openStream()
bitmap = BitmapFactory.decodeStream(inputStream)
} catch (ex: java.lang.Exception) {
ex.printStackTrace()
}
}
thread.start()
thread.join()
/**
* returns the scaled bitmap if it got fetched successfully
*/
return try {
val resizedBitmap = Bitmap.createScaledBitmap(
bitmap,
bitmap.width,
bitmap.width,
false
)
resizedBitmap
} catch (e: Exception) {
null
}
}
}

View File

@ -0,0 +1,180 @@
package com.github.libretube.util
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.support.v4.media.session.MediaSessionCompat
import com.github.libretube.BACKGROUND_CHANNEL_ID
import com.github.libretube.PLAYER_NOTIFICATION_ID
import com.github.libretube.activities.MainActivity
import com.github.libretube.obj.Streams
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import java.net.URL
class NowPlayingNotification(
private val context: Context,
private val player: ExoPlayer
) {
private var streams: Streams? = null
/**
* The [MediaSessionCompat] for the [streams].
*/
private lateinit var mediaSession: MediaSessionCompat
/**
* The [MediaSessionConnector] to connect with the [mediaSession] and implement it with the [player].
*/
private lateinit var mediaSessionConnector: MediaSessionConnector
/**
* The [PlayerNotificationManager] to load the [mediaSession] content on it.
*/
private var playerNotification: PlayerNotificationManager? = null
/**
* The [DescriptionAdapter] is used to show title, uploaderName and thumbnail of the video in the notification
* Basic example [here](https://github.com/AnthonyMarkD/AudioPlayerSampleTest)
*/
inner class DescriptionAdapter() :
PlayerNotificationManager.MediaDescriptionAdapter {
/**
* sets the title of the notification
*/
override fun getCurrentContentTitle(player: Player): CharSequence {
// return controller.metadata.description.title.toString()
return streams?.title!!
}
/**
* overrides the action when clicking the notification
*/
override fun createCurrentContentIntent(player: Player): PendingIntent? {
// return controller.sessionActivity
/**
* starts a new MainActivity Intent when the player notification is clicked
* it doesn't start a completely new MainActivity because the MainActivity's launchMode
* is set to "singleTop" in the AndroidManifest (important!!!)
* that's the only way to launch back into the previous activity (e.g. the player view
*/
val intent = Intent(context, MainActivity::class.java)
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
}
/**
* the description of the notification (below the title)
*/
override fun getCurrentContentText(player: Player): CharSequence? {
// return controller.metadata.description.subtitle.toString()
return streams?.uploader
}
/**
* return the icon/thumbnail of the video
*/
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
lateinit var bitmap: Bitmap
/**
* running on a new thread to prevent a NetworkMainThreadException
*/
val thread = Thread {
try {
/**
* try to GET the thumbnail from the URL
*/
val inputStream = URL(streams?.thumbnailUrl).openStream()
bitmap = BitmapFactory.decodeStream(inputStream)
} catch (ex: java.lang.Exception) {
ex.printStackTrace()
}
}
thread.start()
thread.join()
/**
* returns the scaled bitmap if it got fetched successfully
*/
return try {
val resizedBitmap = Bitmap.createScaledBitmap(
bitmap,
bitmap.width,
bitmap.width,
false
)
resizedBitmap
} catch (e: Exception) {
null
}
}
}
/**
* Creates a [MediaSessionCompat] amd a [MediaSessionConnector] for the player
*/
private fun createMediaSession() {
if (this::mediaSession.isInitialized) return
mediaSession = MediaSessionCompat(context, this.javaClass.name)
mediaSession.isActive = true
mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setPlayer(player)
}
/**
* Updates or creates the [playerNotification]
*/
fun updatePlayerNotification(
streams: Streams
) {
this.streams = streams
if (playerNotification == null) {
createMediaSession()
createNotification()
}
}
/**
* Initializes the [playerNotification] attached to the [player] and shows it.
*/
private fun createNotification() {
playerNotification = PlayerNotificationManager
.Builder(context, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID)
// set the description of the notification
.setMediaDescriptionAdapter(
DescriptionAdapter()
)
.build()
playerNotification?.apply {
setPlayer(player)
setUseNextAction(false)
setUsePreviousAction(false)
setUseStopAction(true)
setColorized(true)
setMediaSessionToken(mediaSession.sessionToken)
}
}
/**
* Destroy the [NowPlayingNotification]
*/
fun destroy() {
mediaSession.isActive = false
mediaSession.release()
mediaSessionConnector.setPlayer(null)
playerNotification?.setPlayer(null)
val notificationManager = context.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.cancel(PLAYER_NOTIFICATION_ID)
player.release()
}
}