androidx / media

Jetpack Media3 support libraries for media use cases, including ExoPlayer, an extensible media player for Android
https://developer.android.com/media/media3
Apache License 2.0
1.62k stars 384 forks source link

Bluetooth headset handle a command custom way #249

Open baruckis opened 1 year ago

baruckis commented 1 year ago

Hi, I would like to handle Bluetooth headset commands COMMAND_SEEK_TO_PREVIOUS and COMMAND_SEEK_TO_NEXT differently. Instead of jumping back to the previous item or next item from the playlist, I want to only seek back and seek forward in the current item by a specific number of seconds (common behaviour in podcasts apps). I have followed your documentation note on how to do that https://developer.android.com/guide/topics/media/media3/getting-started/playing-in-background?authuser=3#handling-ui

The problem is if I have only one item in my playlist or my item is last in the playlist. Then COMMAND_SEEK_TO_NEXT is not available because there is no next item. So I can not catch this command and re-map action. I could set player.repeatMode = Player.REPEAT_MODE_ALL to have button next enabled but that is also not acceptable to repeat content always. Any ideas what else I can try to do? Or is this missing funcionallity?

 override fun onPlayerCommandRequest(session: MediaSession, controller: ControllerInfo, playerCommand: Int): Int {
      Log.i("TESTING", "onPlayerCommandRequest $playerCommand")

      return if (controller.packageName.contains("com.android.bluetooth")) {
        when (playerCommand) {
          Player.COMMAND_SEEK_TO_PREVIOUS -> {
            session.player.seekBack()
            SessionResult.RESULT_INFO_SKIPPED
          }
          Player.COMMAND_SEEK_TO_NEXT -> {
            session.player.seekForward()
            SessionResult.RESULT_INFO_SKIPPED
          }
          else -> super.onPlayerCommandRequest(session, controller, playerCommand)
        }
      } else {
        super.onPlayerCommandRequest(session, controller, playerCommand)
      }
    }
marcbaechinger commented 1 year ago

I think this is difficult to achieve indeed.

When I test with two different BT headsets I see that the skip to next and skip to previous user interactions (like double and triple press the play button) emits a KEYCODE_MEDIA_NEXT or KEYCODE_MEDIA_PREVIOUS. These have the values 87 or 88 respectively.

I then see that your analysis is correct: Media3 in aware of this MediaButtonEvent in MediaSessionLegacyStub.onMediaButtonEvent(...), but it is not handled there but instead left to the system to be handled (the callback method returns false to indicate to the system the event isn't handled).

The system then checks the actions of the platform media session, sees the action [ACTION_SKIP_TO_NEXT](https://developer.android.com/reference/android/support/v4/media/session/PlaybackStateCompat#ACTION_SKIP_TO_NEXT()) is not available and then drops the command, meaning the corresponding MediaSession.Callback method is not called by the system.

Changing this behaviour in the Media3 library is technically possible, but I'm not sure how sensible that would be. We had to handle the key event in MediaSessionLegacyStub.onMediaButtonEvent ouself and then process some specific logic. But then we would break the vailable commands design ourself.

You could use a ForwardingPlayer that wraps the actual player, overrides getAvailableCommands(), and intercepts the listeners to customize onAvailableCommands and then tells the session that the COMMAND_SEEK_TO_NEXT is always available. However, this would change the available actions in the platform session (see adb shell dumpsys media_session) are shared across all users of the legacy session. So you would change this behaviour for all clients which includes for instance SystemUI (the media notification), Bluetooth clients, Android Auto and any third party app that is connecting with your platform session. All these potential clients setup their UI (like the next button) according to these actions. They could also look at the queue and notice that we are already on the last item. But I wouldn't rely on this behaviour specifically, because you had to test this on various OS versions and API levels.

So even if this is probably not a very helpful answer, I think that unless the BT headset is actually sending a KEYCODE_MEDIA_FAST_FORWARD, I don't know how this could be achieved.

Have you been able to do that with the legacy API?

baruckis commented 1 year ago

Hey thank you for your reply and that overviewed possible scenarios. That is useful. I was trying to implement a workaround with Player.REPEAT_MODE_ALL after your reply but in general it does not work well for us. I have not tried to do this on legacy API, we have full focus now just on media3 as our main service. However, our users are requesting this functionality by leaving negative app reviews. When they are listening to podcasts on headphones, they expect to just seek forward or backward by a few seconds. This behavior is on the Google podcasts app or Spotify podcasts, so it is what they expect.

y20k commented 1 year ago

Hi @marcbaechinger

You could use a ForwardingPlayer that wraps the actual player, overrides getAvailableCommands(), and intercepts the listeners to customize onAvailableCommands and then tells the session that the COMMAND_SEEK_TO_NEXT is always available.

This how I tried to solve the same issue: Intercepting the headphone issued skip commands for a podcast player app.

The thing is: this method used to work in 1.0.0-beta03 and stopped working in 1.0.0-rc01.

marcbaechinger commented 1 year ago

Can you expand on what is not working anymore? Best would be to file a new issue.

y20k commented 1 year ago

Sorry. I meant, I was doing the exact thing, that you described as a workaround:

I was using a ForwardingPlayer to override getAvailableCommands() to ensure that COMMAND_SEEK_TO_NEXT is always available.

Best would be to file a new issue.

Sure. I will do that. I just suspected it was an deliberate change not a bug.

abouda commented 1 year ago

Same issue here with Media3 v1.1.0 I don't want to use Player.REPEAT_MODE_ALL since that will add skip next/previous buttons to my notification/android auto. My app only plays 1 audio file at a time (so no need for queue). I have 2 custom actions for fast-forward/rewind.

image

Is there a way to have the skip commands active in the session without displaying them in the notification? Or is there a way to inject my own MediaButtonReceiver to handle the skip events from the headset?

marcbaechinger commented 1 year ago

Is there a way to have the skip commands active in the session without displaying them in the notification?

I understand with skip commands active in the session you mean to keep them in PlaybackStateCompat.getActions() but not showing them in the notification.

Technically you can provide your own MediaNotificationProvider or override DefaultMediaNotificationManager.getMediaButtons. There you can define what buttons to show in the notification and keep the commands available with the ForwardingPlayer and hence for the platform session.

This can probably solve your problem with API 32 and below. However, with API 33 and later, the actions in the notification are not relevant, instead System UI uses the actions in the PlaybackStateCompat, so I think the buttons would still show up on API 33.

abouda commented 1 year ago

That doesn't feel like an optimal solution then.

I can see from the logs that my headset is sending Sending KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_MEDIA_NEXT and Sending KeyEvent { action=ACTION_UP, keyCode=KEYCODE_MEDIA_NEXT, to androidx.media3.session.id. And from the androidx.media3.session.MediaSession docs:

Media Key Events Mapping When the session receives media key events they are mapped to a method call on the underlying player. KeyEvent.KEYCODE_MEDIA_NEXT -> Player.seekToNext()

But my ForwardingPlayer´s seekToNext() is not called unless I add COMMAND_SEEK_TO_PREVIOUS and COMMAND_SEEK_TO_NEXT or use Player.REPEAT_MODE_ALL. Both which will make the notification display skip buttons in addition to my Custom seek actions.

marcbaechinger commented 1 year ago

unless I add COMMAND_SEEK_TO_PREVIOUS and COMMAND_SEEK_TO_NEXT

When a player is saying to not support for instance COMMAND_SEEK_TO_PREVIOUS then it looks correct to me to not deliver these messages even when a source send such events. The player says it is not supported by not providing the available command for it.

A session can have arbitrary implementation of the Player interface. If such a player implementation tells the session to not support COMMAND_SEEK_TO_PREVIOUS, then your statement 'my ForwardingPlayer´s seekToNext() is not called unless I add COMMAND_SEEK_TO_PREVIOUS and COMMAND_SEEK_TO_NEXT' seems to confirm that this is implemented as intended.

abouda commented 1 year ago

Understood. It sounds like it is working as intended. But that is not how Google Podcasts app works. I've checked (in Media Controller Tester) that their MediaSession has COMMAND_SEEK_TO_PREVIOUS and COMMAND_SEEK_TO_NEXT inactivated. Yet they support seek next/previous media buttons and handle them as ffw/rewind.

image

If you have any insights on wether this is doable with media3 would be appreciated.

JureSencar commented 1 year ago

One possibility is to intercept the key intent in onStartCommand in MediaSessionService and handle NEXT/PREVIOUS there.