From e25cbf343b56c8a3d69b25a9ca2e051203f376e6 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 7 Aug 2022 18:10:13 +0200 Subject: [PATCH 1/6] improve player notification --- .../libretube/services/BackgroundMode.kt | 109 ++++++++++++++---- 1 file changed, 85 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt index 0431263e8..66b5ab73a 100644 --- a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt +++ b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt @@ -3,9 +3,11 @@ package com.github.libretube.services import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager +import android.app.PendingIntent import android.app.Service -import android.content.Context import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.os.Build import android.os.Handler import android.os.IBinder @@ -15,6 +17,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.github.libretube.BACKGROUND_CHANNEL_ID import com.github.libretube.PLAYER_NOTIFICATION_ID import com.github.libretube.R +import com.github.libretube.activities.MainActivity import com.github.libretube.obj.Segment import com.github.libretube.obj.Segments import com.github.libretube.obj.Streams @@ -35,6 +38,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.net.URL /** * Loads the selected videos audio in background mode with a notification area. @@ -106,9 +110,6 @@ class BackgroundMode : Service() { * Initializes the [player] with the [MediaItem]. */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - // destroy the old player - destroyPlayer() - // get the intent arguments videoId = intent?.getStringExtra("videoId")!! val position = intent.getLongExtra("position", 0L) @@ -194,9 +195,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,6 +202,85 @@ class BackgroundMode : Service() { } } + /** + * 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 response?.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(this@BackgroundMode, MainActivity::class.java) + return PendingIntent.getActivity(this@BackgroundMode, 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 response?.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(response?.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 + } + } + } + /** * Initializes the [playerNotification] attached to the [player] and shows it. */ @@ -212,12 +289,7 @@ class BackgroundMode : Service() { .Builder(this, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID) // set the description of the notification .setMediaDescriptionAdapter( - DescriptionAdapter( - response?.title!!, - response?.uploader!!, - response?.thumbnailUrl!!, - this - ) + DescriptionAdapter() ) .build() playerNotification?.apply { @@ -284,17 +356,6 @@ 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) - } - override fun onBind(p0: Intent?): IBinder? { TODO("Not yet implemented") } From 9c3436751c21da89927187a679b7bd0364b0aeb5 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 7 Aug 2022 18:22:40 +0200 Subject: [PATCH 2/6] external notification class --- .../libretube/services/BackgroundMode.kt | 125 ++-------------- .../libretube/util/NowPlayingNotification.kt | 136 ++++++++++++++++++ 2 files changed, 147 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt diff --git a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt index 66b5ab73a..38481014b 100644 --- a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt +++ b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt @@ -3,11 +3,8 @@ package com.github.libretube.services import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager -import android.app.PendingIntent import android.app.Service import android.content.Intent -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.os.Build import android.os.Handler import android.os.IBinder @@ -17,13 +14,12 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.github.libretube.BACKGROUND_CHANNEL_ID import com.github.libretube.PLAYER_NOTIFICATION_ID import com.github.libretube.R -import com.github.libretube.activities.MainActivity 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.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 @@ -33,12 +29,10 @@ 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 import kotlinx.coroutines.runBlocking -import java.net.URL /** * Loads the selected videos audio in background mode with a notification area. @@ -70,11 +64,6 @@ class BackgroundMode : Service() { */ 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] */ @@ -85,6 +74,11 @@ class BackgroundMode : Service() { */ private var segmentData: Segments? = null + /** + * Notification for the player + */ + private lateinit var nowPlayingNotification: NowPlayingNotification + override fun onCreate() { super.onCreate() /** @@ -134,7 +128,11 @@ class BackgroundMode : Service() { job.join() initializePlayer() - initializePlayerNotification() + setMediaItem() + + // create the notification + nowPlayingNotification = NowPlayingNotification(this@BackgroundMode, player!!) + nowPlayingNotification.initializePlayerNotification(mediaSession, response!!) player?.apply { playWhenReady = playWhenReadyPlayer @@ -184,7 +182,6 @@ class BackgroundMode : Service() { } } }) - setMediaItem() } /** @@ -202,106 +199,6 @@ class BackgroundMode : Service() { } } - /** - * 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 response?.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(this@BackgroundMode, MainActivity::class.java) - return PendingIntent.getActivity(this@BackgroundMode, 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 response?.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(response?.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 - } - } - } - - /** - * 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() - ) - .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]. diff --git a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt new file mode 100644 index 000000000..f9a8638ae --- /dev/null +++ b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt @@ -0,0 +1,136 @@ +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 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.ui.PlayerNotificationManager +import java.net.URL + +class NowPlayingNotification( + private val context: Context, + private val player: ExoPlayer +) { + private var streams: Streams? = null + + /** + * The [PlayerNotificationManager] to load the [mediaSession] content on it. + */ + private var playerNotification: PlayerNotificationManager? = null + + private fun setStreams(streams: Streams) { + this.streams = streams + } + + /** + * 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 + } + } + } + + /** + * Initializes the [playerNotification] attached to the [player] and shows it. + */ + fun initializePlayerNotification( + mediaSession: MediaSessionCompat, + streams: Streams + ) { + this.streams = streams + 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) + } + } +} From 3399e4a8a820691f88e59c2da7925583ea7ed7cc Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 7 Aug 2022 19:01:03 +0200 Subject: [PATCH 3/6] use proper way --- .../libretube/services/BackgroundMode.kt | 38 +++++++------------ .../libretube/util/NowPlayingNotification.kt | 34 ++++++++++++++--- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt index 38481014b..e6ee2dcd7 100644 --- a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt +++ b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt @@ -9,7 +9,6 @@ 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 @@ -54,16 +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 [AudioAttributes] handle the audio focus of the [player] */ @@ -104,12 +93,17 @@ class BackgroundMode : Service() { * Initializes the [player] with the [MediaItem]. */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - // get the intent arguments - videoId = intent?.getStringExtra("videoId")!! - val position = intent.getLongExtra("position", 0L) + try { + // 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) } @@ -131,8 +125,10 @@ class BackgroundMode : Service() { setMediaItem() // create the notification - nowPlayingNotification = NowPlayingNotification(this@BackgroundMode, player!!) - nowPlayingNotification.initializePlayerNotification(mediaSession, response!!) + if (!this@BackgroundMode::nowPlayingNotification.isInitialized) { + nowPlayingNotification = NowPlayingNotification(this@BackgroundMode, player!!) + } + nowPlayingNotification.updatePlayerNotification(response!!) player?.apply { playWhenReady = playWhenReadyPlayer @@ -208,12 +204,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) } /** diff --git a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt index f9a8638ae..83b090ed4 100644 --- a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt +++ b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt @@ -12,6 +12,7 @@ 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 @@ -21,15 +22,21 @@ class NowPlayingNotification( ) { 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 - private fun setStreams(streams: Streams) { - this.streams = streams - } - /** * The [DescriptionAdapter] is used to show title, uploaderName and thumbnail of the video in the notification * Basic example [here](https://github.com/AnthonyMarkD/AudioPlayerSampleTest) @@ -109,14 +116,29 @@ class NowPlayingNotification( } } + private fun createMediaSession() { + if (this::mediaSession.isInitialized) return + mediaSession = MediaSessionCompat(context, this.javaClass.name) + mediaSession.isActive = true + + mediaSessionConnector = MediaSessionConnector(mediaSession) + mediaSessionConnector.setPlayer(player) + } + /** * Initializes the [playerNotification] attached to the [player] and shows it. */ - fun initializePlayerNotification( - mediaSession: MediaSessionCompat, + fun updatePlayerNotification( streams: Streams ) { this.streams = streams + if (playerNotification == null) { + createMediaSession() + createNotification() + } + } + + private fun createNotification() { playerNotification = PlayerNotificationManager .Builder(context, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID) // set the description of the notification From 955d6de69028b7be97c6e97810a75b90442b5e43 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 7 Aug 2022 19:10:16 +0200 Subject: [PATCH 4/6] use for player fragment too --- .../libretube/fragments/PlayerFragment.kt | 57 +++-------- .../libretube/util/DescriptionAdapter.kt | 95 ------------------- .../libretube/util/NowPlayingNotification.kt | 13 +++ 3 files changed, 24 insertions(+), 141 deletions(-) delete mode 100644 app/src/main/java/com/github/libretube/util/DescriptionAdapter.kt diff --git a/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt index 40c2e8cbc..8831af7de 100644 --- a/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt @@ -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 @@ -60,7 +56,7 @@ import com.github.libretube.services.BackgroundMode 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 @@ -181,9 +175,7 @@ class PlayerFragment : Fragment() { /** * 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 +668,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,7 +736,7 @@ 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() @@ -1502,33 +1486,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 diff --git a/app/src/main/java/com/github/libretube/util/DescriptionAdapter.kt b/app/src/main/java/com/github/libretube/util/DescriptionAdapter.kt deleted file mode 100644 index 6bc27bf40..000000000 --- a/app/src/main/java/com/github/libretube/util/DescriptionAdapter.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt index 83b090ed4..3959438a6 100644 --- a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt +++ b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt @@ -1,5 +1,6 @@ package com.github.libretube.util +import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -155,4 +156,16 @@ class NowPlayingNotification( setMediaSessionToken(mediaSession.sessionToken) } } + + 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() + } } From 6945f29282d38f37f71a5fd32bcc8658526721ee Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 7 Aug 2022 19:15:23 +0200 Subject: [PATCH 5/6] add documentation --- .../libretube/services/BackgroundMode.kt | 25 +++++++++++++------ .../libretube/util/NowPlayingNotification.kt | 11 +++++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt index e6ee2dcd7..28fefb948 100644 --- a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt +++ b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt @@ -68,11 +68,11 @@ class BackgroundMode : Service() { */ 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( @@ -169,11 +169,7 @@ 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() } } } @@ -243,6 +239,19 @@ class BackgroundMode : Service() { } } + /** + * 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? { TODO("Not yet implemented") } diff --git a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt index 3959438a6..f0f52809c 100644 --- a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt +++ b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt @@ -117,6 +117,9 @@ class NowPlayingNotification( } } + /** + * Creates a [MediaSessionCompat] amd a [MediaSessionConnector] for the player + */ private fun createMediaSession() { if (this::mediaSession.isInitialized) return mediaSession = MediaSessionCompat(context, this.javaClass.name) @@ -127,7 +130,7 @@ class NowPlayingNotification( } /** - * Initializes the [playerNotification] attached to the [player] and shows it. + * Updates or creates the [playerNotification] */ fun updatePlayerNotification( streams: Streams @@ -139,6 +142,9 @@ class NowPlayingNotification( } } + /** + * Initializes the [playerNotification] attached to the [player] and shows it. + */ private fun createNotification() { playerNotification = PlayerNotificationManager .Builder(context, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID) @@ -157,6 +163,9 @@ class NowPlayingNotification( } } + /** + * Destroy the [NowPlayingNotification] + */ fun destroy() { mediaSession.isActive = false mediaSession.release() From 0f679d68bc93c0deadb22b419f42487c34219e3c Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 7 Aug 2022 20:01:22 +0200 Subject: [PATCH 6/6] refactor autoplay --- .../libretube/fragments/PlayerFragment.kt | 74 +++++-------------- .../github/libretube/util/AutoPlayHelper.kt | 37 ++++++++++ 2 files changed, 54 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt diff --git a/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt index 8831af7de..a2cbf2544 100644 --- a/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt @@ -46,13 +46,13 @@ 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 @@ -169,8 +169,7 @@ class PlayerFragment : Fragment() { * for autoplay */ private var nextStreamId: String? = null - private var playlistStreamIds: MutableList = arrayListOf() - private var playlistNextPage: String? = null + private lateinit var autoPlayHelper: AutoPlayHelper /** * for the player notification @@ -741,7 +740,7 @@ class PlayerFragment : Fragment() { // show comments if related streams disabled if (!relatedStreamsEnabled) toggleComments() // prepare for autoplay - initAutoPlay() + if (autoplayEnabled) setNextStream() if (watchHistoryEnabled) { PreferenceHelper.addToWatchHistory(videoId!!, streams) } @@ -751,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 */ @@ -808,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 diff --git a/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt b/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt new file mode 100644 index 000000000..c1e82a491 --- /dev/null +++ b/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt @@ -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() + 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 + } +}