I am trying to implement ExoPlayer's Notification Manager, it works pretty well but I do not want to show fast rewind and fast forward buttons. I checked documentation but can not find a way to hide these button. Is there any tricky way to hide them?
Here is my code
private fun initListener() {
val playerNotificationManager: PlayerNotificationManager
val notificationId = 1234
val mediaDescriptionAdapter = object : PlayerNotificationManager.MediaDescriptionAdapter {
override fun getCurrentSubText(player: Player?): String {
return "Sub text"
}
override fun getCurrentContentTitle(player: Player): String {
return "Title"
}
override fun createCurrentContentIntent(player: Player): PendingIntent? {
return null
}
override fun getCurrentContentText(player: Player): String {
return "ContentText"
}
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
return null
}
}
playerNotificationManager = PlayerNotificationManager.createWithNotificationChannel(
context,
"My_channel_id",
R.string.app_name,
notificationId,
mediaDescriptionAdapter,
object : PlayerNotificationManager.NotificationListener {
override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) {}
override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {}
})
playerNotificationManager.setUseNavigationActions(false)
playerNotificationManager.setUseNavigationActionsInCompactView(false)
playerNotificationManager.setVisibility(View.VISIBLE)
playerNotificationManager.setPlayer(mPlayer)
}
You can set rewindIncrementMs and fastForwardIncrementMs to 0 to hide the buttons.
The link to the JavaDoc you posted above explaines this: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/PlayerNotificationManager.html
playerNotificationManager.setRewindIncrementMs(0);
playerNotificationManager.setFastForwardIncrementMs(0);
You can do this in ExoPlayer 2.15.0 -
playerNotificationManager.setUseFastForwardAction(false)
playerNotificationManager.setUseFastForwardActionInCompactView(false)
playerNotificationManager.setUseRewindAction(false)
playerNotificationManager.setUseRewindActionInCompactView(false)
Related
I am working on a native music player app for android using ExoPlayer and MediaSessionService from Media3. Now I want to make playback more energy efficient while the screen is off by using experimentalSetOffloadSchedulingEnabled, but it seems like I’m not able to get the offloading to work.
From the main activity of the app I send ACTION_START_AUDIO_OFFLOAD in the onStop() method to my service (the relevant parts of the service are show below), and ACTION_STOP_AUDIO_OFFLOAD in the onStart() method. In this way I have been able to get correct true/false responses from the onExperimentalOffloadSchedulingEnabledChanged listener, but I do not get any responses from the onExperimentalOffloadedPlayback or onExperimentalSleepingForOffloadChanged listeners, so it seems like the player never enters power saving mode.
My tests were made with Media3 version 1.0.0-beta03 on Android 13 (emulator) and Android 10 (phone) using MP3 files. I am aware that Media3 is in beta and that the offload scheduling method is experimental, but I'm not sure if that is the limitation or if I have done something wrong. Any ideas what could be the issue?
#androidx.media3.common.util.UnstableApi
class PlaybackService: MediaSessionService(), MediaSession.Callback {
private val listener = object : ExoPlayer.AudioOffloadListener {
override fun onExperimentalOffloadSchedulingEnabledChanged(offloadSchedulingEnabled: Boolean) {
Log.d("PlaybackService","offloadSchedulingEnabled: $offloadSchedulingEnabled")
super.onExperimentalOffloadSchedulingEnabledChanged(offloadSchedulingEnabled)
}
override fun onExperimentalOffloadedPlayback(offloadedPlayback: Boolean) {
Log.d("PlaybackService","offloadedPlayback: $offloadedPlayback")
super.onExperimentalOffloadedPlayback(offloadedPlayback)
}
override fun onExperimentalSleepingForOffloadChanged(sleepingForOffload: Boolean) {
Log.d("PlaybackService","sleepingForOffload: $sleepingForOffload")
super.onExperimentalSleepingForOffloadChanged(sleepingForOffload)
}
}
private lateinit var player: ExoPlayer
private var mediaSession: MediaSession? = null
override fun onCreate() {
super.onCreate()
player = ExoPlayer.Builder(
this,
DefaultRenderersFactory(this)
.setEnableAudioOffload(true)
)
.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus = */ true)
.setHandleAudioBecomingNoisy(true)
.setSeekBackIncrementMs(10_000)
.setSeekForwardIncrementMs(10_000)
.setWakeMode(C.WAKE_MODE_LOCAL)
.build()
player.addAudioOffloadListener(listener)
mediaSession = MediaSession
.Builder(this, player)
.setCallback(this)
.build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
mediaSession
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when(intent?.action) {
ACTION_START_AUDIO_OFFLOAD -> startAudioOffload()
ACTION_STOP_AUDIO_OFFLOAD -> stopAudioOffload()
}
return super.onStartCommand(intent, flags, startId)
}
private fun startAudioOffload() {
player.experimentalSetOffloadSchedulingEnabled(true)
}
private fun stopAudioOffload() {
player.experimentalSetOffloadSchedulingEnabled(false)
}
override fun onDestroy() {
mediaSession?.run {
player.release()
release()
mediaSession = null
}
super.onDestroy()
}
companion object {
const val ACTION_START_AUDIO_OFFLOAD = "ACTION_START_AUDIO_OFFLOAD"
const val ACTION_STOP_AUDIO_OFFLOAD = "ACTION_STOP_AUDIO_OFFLOAD"
}
}
I build a web radio player with Media3 1.0.0-beta03. I use the sample code from
Developers page.
It's generated a media notification automatically but I don't know how to add Title and sub title to this.
Here is my media service:
class PlaybackService : MediaSessionService(), MediaSession.Callback {
private object LC {
lateinit var exoPlayer: ExoPlayer
lateinit var mediaSession: MediaSession
}
override fun onCreate() {
super.onCreate()
log("----------------------------- MediaSessionService, onCreate")
LC.exoPlayer = ExoPlayer.Builder(this).build()
LC.exoPlayer.addListener(ExoListener())
LC.exoPlayer.setAudioAttributes(AudioAttributes.Builder().setContentType(AUDIO_CONTENT_TYPE_MUSIC).setUsage(USAGE_MEDIA).build(),true)
LC.mediaSession = MediaSession.Builder(this, LC.exoPlayer).setCallback(this).build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession = LC.mediaSession
override fun onAddMediaItems(mediaSession: MediaSession, controller: MediaSession.ControllerInfo, mediaItems: MutableList<MediaItem>): ListenableFuture<MutableList<MediaItem>> {
val updatedMediaItems = mediaItems.map { it.buildUpon().setUri(it.mediaId).build() }.toMutableList()
return Futures.immediateFuture(updatedMediaItems)
}
override fun onDestroy() {
log("----------------------------- MediaSessionService, onDestroy")
LC.exoPlayer.stop()
LC.exoPlayer.release()
LC.mediaSession.release()
super.onDestroy()
exitProcess(0)
}
}
I tryed the onUpdateNotification
Yesss, thank you TG. Kahsay
Notification manager is not needed.
class PlaybackService : MediaSessionService(), MediaSession.Callback {
private object LC {
lateinit var exoPlayer: ExoPlayer
lateinit var mediaSession: MediaSession
}
#SuppressLint("UnsafeOptInUsageError")
override fun onCreate() {
super.onCreate()
log("----------------------------- MediaSessionService, onCreate")
LC.exoPlayer = ExoPlayer.Builder(this).build()
LC.exoPlayer.addListener(BackgroundService())
LC.exoPlayer.setAudioAttributes(AudioAttributes.Builder().setContentType(AUDIO_CONTENT_TYPE_MUSIC).setUsage(USAGE_MEDIA).build(),true)
LC.mediaSession = MediaSession.Builder(this, LC.exoPlayer).setCallback(this).build()
setMediaNotificationProvider(object : MediaNotification.Provider{
override fun createNotification(
mediaSession: MediaSession,
customLayout: ImmutableList<CommandButton>,
actionFactory: MediaNotification.ActionFactory,
onNotificationChangedCallback: MediaNotification.Provider.Callback
): MediaNotification {
// This run every time when I press buttons on notification bar:
return updateNotification(mediaSession)
}
override fun handleCustomCommand(session: MediaSession, action: String, extras: Bundle): Boolean { return false }
})
}
#SuppressLint("UnsafeOptInUsageError")
private fun updateNotification(session: MediaSession): MediaNotification {
val notify = NotificationCompat.Builder(this,"Radio")
.setSmallIcon(R.drawable.ic_launcher_foreground)
// This is globally changed every time when
// I add a new MediaItem from background service
.setContentTitle(GL.MEDIA.radio)
.setContentText(GL.MEDIA.artist)
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
.build()
return MediaNotification(1, notify)
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession = LC.mediaSession
override fun onAddMediaItems(mediaSession: MediaSession, controller: MediaSession.ControllerInfo, mediaItems: MutableList<MediaItem>): ListenableFuture<MutableList<MediaItem>> {
val updatedMediaItems = mediaItems.map { it.buildUpon().setUri(it.mediaId).build() }.toMutableList()
return Futures.immediateFuture(updatedMediaItems)
}
override fun onDestroy() {
log("----------------------------- MediaSessionService, onDestroy")
LC.exoPlayer.stop()
LC.exoPlayer.release()
LC.mediaSession.release()
super.onDestroy()
}
}
Update :
There is also another way, which I found out does the Job better.
In onCreate() function of MediaSessionService, We can set a MediaNotificationProvider like so.
private lateinit var nBuilder: NotificationCompat.Builder
override fun onCreate(){
super.onCreate()
// init notificationCompat.Builder before setting the MediaNotificationProvider
this.setMediaNotificationProvider(object : MediaNotification.Provider{
override fun createNotification(
mediaSession: MediaSession,// this is the session we pass to style
customLayout: ImmutableList<CommandButton>,
actionFactory: MediaNotification.ActionFactory,
onNotificationChangedCallback: MediaNotification.Provider.Callback
): MediaNotification {
createNotification(mediaSession)
// notification should be created before you return here
return MediaNotification(NOTIFICATION_ID,nBuilder.build())
}
override fun handleCustomCommand(
session: MediaSession,
action: String,
extras: Bundle
): Boolean {
TODO("Not yet implemented")
}
})
}
fun createNotification(session: MediaSession) {
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(NotificationChannel(notification_id,"Channel", NotificationManager.IMPORTANCE_LOW))
// NotificationCompat.Builder here.
nBuilder = NotificationCompat.Builder(this,notification_id)
// Text can be set here
// but I believe setting MediaMetaData to MediaSession would be enough.
// I havent tested it deeply yet but did display artist from session
.setSmallIcon(R.drawable.your_drawable)
.setContentTitle("your Content title")
.setContentText("your content text")
// set session here
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
// we don build.
}
and finally if you want to update notification info yourself
you can do so by calling a function like this..
private fun updateNotification(/*parameter*/){
nBuilder.setContentTitle("text")
nBuilder.setContentText("subtext")
nManager.notify(NOTIFICATION_ID,nBuilder.build())
}
Turns out, its very simple.
There is a method to override in MediaService class called onUpdateNotification(). it provides the media session for us.
so we can override it and create our own NotificationCompat
// Override this method in your service
override fun onUpdateNotification(session: MediaSession) {
createNotification(session) //calling method where we create notification
}
and in our createNotification() method we create the notification and set its style with a MediaStyleHelper.MediaStyle() set the session parameter there
like in the following.
and create the notification as always
fun createNotification(session: MediaSession) {
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(NotificationChannel(notification_id,"Channel", NotificationManager.IMPORTANCE_LOW))
// NotificationCompat here.
val notificationCompat = NotificationCompat.Builder(this,notification_id)
// Text can be set here
// but I believe setting MediaMetaData to MediaSession would be enough.
// I havent tested it deeply yet but did display artist from session
.setSmallIcon(R.drawable.your_drawable)
.setContentTitle("your Content title")
.setContentText("your content text")
// set session here
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
.build()
notificationManager.notify(1,notificationCompat)
}
I hope this helps and isn't too late.
Edit:
and another cleaner option is to just Create MediaItem wit desired MediaMetaData and add it to ExoPlayer. If source is Hls, try not adding title to MediaMetaData.
I want to use custom next/previous actions instead of the native ones. So I started by removing the native ones by allowing only needed actions:
private inner class CustomQueueNavigator(
mediaSession: MediaSessionCompat
) : TimelineQueueNavigator(mediaSession) {
override fun getSupportedQueueNavigatorActions(player: Player): Long {
return PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM or
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
}
}
And then I added my custom actions:
MediaSessionConnector(mediaSession).setCustomActionProviders(object :
MediaSessionConnector.CustomActionProvider {
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {}
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
return PlaybackStateCompat.CustomAction.Builder(
"SKIP_TO_PREVIOUS_ACTION",
"previous",
if (!isFirst)
R.drawable.ic_previous_with_padding
else R.drawable.ic_previous_disabled_with_padding
).build()
}
},
object : MediaSessionConnector.CustomActionProvider {
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {}
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
return PlaybackStateCompat.CustomAction.Builder(
"SKIP_TO_NEXT_ACTION",
"next",
if (!isLast)
R.drawable.ic_next_with_padding
else R.drawable.ic_next_disabled_with_padding
).build()
}
}
Results:
In app:
In home:
As you observed, the buttons in the home section in the automotive are inverted, how can I keep the same order as in the app?
I want to implement an app that can convert text to speech and record videos (with audio) simultaneously.
But when I am calling both function either one of them working (the recent one that has been called). Can anyone suggest some ways to implement these two together.
`
speechRecognizer = SpeechRecognizer.createSpeechRecognizer(activity)
val speechRecognizerIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
speechRecognizerIntent.putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,
)
speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,10000)
// speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_MINIMUM_LENGTH_MILLIS,30000)
speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault())
speechRecognizerIntent.putExtra("android.speech.extra.GET_AUDIO_FORMAT", "audio/MP3")
speechRecognizerIntent.putExtra("android.speech.extra.GET_AUDIO", true)
speechRecognizer.setRecognitionListener(object : RecognitionListener {
override fun onReadyForSpeech(bundle: Bundle?) {
speechRecognizer.startListening(speechRecognizerIntent)
}
override fun onBeginningOfSpeech() {}
override fun onRmsChanged(v: Float) {}
override fun onBufferReceived(bytes: ByteArray?) {}
override fun onEndOfSpeech() {
// changing the color of our mic icon to
// gray to indicate it is not listening
// #FF6D6A6A
}
override fun onError(i: Int) {}
override fun onResults(bundle: Bundle) {
}
override fun onPartialResults(bundle: Bundle) {
val result = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
if (result != null) {
for (i in 0 until result.size){
text.add(result[0])
Log.d("Record",result[0])
//binding.tvtext.text = binding.tvtext.text.toString() + result[0]
}
}
}
override fun onEvent(i: Int, bundle: Bundle?) {}
})
speechRecognizer.startListening(speechRecognizerIntent)
}
`
I'm trying to run exoplayer in a foreground service (not a MediaBrowserServiceCompat).
Here is my service -
#AndroidEntryPoint
class PodcastPlayerService: Service() {
#Inject
lateinit var dataSourceFactory: DefaultDataSourceFactory
#Inject
lateinit var exoPlayer: SimpleExoPlayer
lateinit var podcastNotificationManager: PodcastNotificationManager
lateinit var podcast: Podcast
override fun onDestroy() {
super.onDestroy()
exoPlayer.release()
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
exoPlayer.stop()
exoPlayer.release()
}
override fun onBind(p0: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("Pit stop", "2")
val b = intent!!.getBundleExtra("test")
if (b != null) {
Log.i("Pit stop", "3")
podcast = b.getParcelable<Podcast>(ArgumentKeyAndValues.KEY_PODCAST)!!
}
val mediaSource = buildMediaSource(Uri.parse("https://something.etc/file.mp3"))
if (mediaSource != null) {
exoPlayer.prepare(mediaSource)
exoPlayer.playWhenReady = true
podcastNotificationManager =
PodcastNotificationManager(
this,
PodcastPlayerNotificationListener(this)
)
podcastNotificationManager.showNotification(exoPlayer)
}
return START_STICKY
}
private fun buildMediaSource(uri: Uri): MediaSource? {
return ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(uri)
}
}
Notification Listener -
class PodcastPlayerNotificationListener(private val podcastPlayerService: PodcastPlayerService):
PlayerNotificationManager.NotificationListener {
override fun onNotificationPosted(
notificationId: Int,
notification: Notification,
ongoing: Boolean) {
super.onNotificationPosted(notificationId, notification, ongoing)
podcastPlayerService.apply {
if(ongoing) {
ContextCompat.startForegroundService(this,
Intent(applicationContext, this::class.java))
startForeground(OtherConstants.PODCAST_NOTIFICATION_ID, notification)
}
}
}
override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
super.onNotificationCancelled(notificationId, dismissedByUser)
podcastPlayerService.apply {
stopForeground(true)
stopSelf()
}
}
}
Podcast Notification Manager -
class PodcastNotificationManager(private val context: Context,
notificationListener: PlayerNotificationManager.NotificationListener) {
private val notificationManager: PlayerNotificationManager
init {
notificationManager = PlayerNotificationManager.createWithNotificationChannel(
context,
OtherConstants.NOTIFICATION_CHANNEL_ID,
R.string.notification_channel_name,
R.string.notification_channel_description,
OtherConstants.PODCAST_NOTIFICATION_ID,
DescriptionAdapter(),
notificationListener
).apply {
setSmallIcon(R.drawable.exo_icon_play)
}
}
fun showNotification(player: Player) {
notificationManager.setPlayer(player)
}
private inner class DescriptionAdapter : PlayerNotificationManager.MediaDescriptionAdapter {
override fun getCurrentContentTitle(player: Player): String {
val window = player.currentWindowIndex
return "Title"
}
override fun getCurrentContentText(player: Player): String? {
val window = player.currentWindowIndex
return "Description"
}
override fun getCurrentLargeIcon(
player: Player,
callback: BitmapCallback
): Bitmap? = null
override fun createCurrentContentIntent(player: Player): PendingIntent? {
val window = player.currentWindowIndex
return null
}
}
}
Here is how I start the service -
val intent = Intent(context, PodcastPlayerService::class.java)
val serviceBundle = Bundle()
serviceBundle.putParcelable("test", podcast)
intent.putExtra(ArgumentKeyAndValues.KEY_PODCAST, serviceBundle)
context?.let { Util.startForegroundService(it, intent) }
However, when I do this onStartCommand keeps getting called (I'm assuming that the OS keeps killing my service for some reason and START_STICKY forces it to start again) and nothing happens.
If I place the media, notification manager and listener code in onCreate the service works fine.
Where am I going wrong?
Turns out I was starting the foreground service twice -
ContextCompat.startForegroundService(this,
Intent(applicationContext, this::class.java))
startForeground(OtherConstants.PODCAST_NOTIFICATION_ID, notification)