[Twoja aplikacja – odtwarzacz audio] MediaSession i MediaController. Część 1

W serii „Twoja aplikacja” będę pokazywał, w jaki sposób stworzyć aplikację kompletną wraz z najważniejszymi komponentami. Taka aplikacja będzie posiadać wszystkie podstawowe rzeczy, która powinna mieć. Seria będzie podzielona na części, a każda część będzie zawierać poszczególne zagadnienie.

 
W tej serii stworzymy aplikację o odtwarzania muzyki wraz z najważniejszymi komponentami.

  1.  
  2. Część 1: MediaSession i MediaController
  3. Cześć 2: AudioFocus
  4. Cześć 3: Powiadomienia

W artykule omówimy architekturę i interfejs API do tworzenia aplikacji odtwarzających muzykę. Napiszemy prostą aplikację, która będzie odtwarzać utwór za pomocą oficjalnie zalecanych praktyk. Będziemy używać MediaSessionMediaController do zorganizowania jednego punktu dostępu do odtwarzacza multimedialnego. Ponadto określę kilka kroków, które są obowiązkowe, jeśli nie chcesz robić problemów użytkownikowi.
Zadanie wygląda na proste, tworzymy MediaPlayer, po naciśnięciu przycisku Play, zaczyna odtwarzanie, Stop – zatrzymuje. Wszystko działa dobrze, dopóki użytkownik nie zamknie aplikacji. Oczywistym rozwiązaniem jest przeniesienie MediaPlayer’a do usługi. Tutaj pojawia się dylemat dotyczący organizacji dostępu do odtwarzacza z interfejsu użytkownika. Będziemy musieli wdrożyć bindservice i stworzyć dla niej API, które pozwoli nam kontrolować odtwarzacz i odbierać z niego zdarzenia. Ale to tylko połowa drogi, nikt poza nami nie zna API usługi. Użytkownik będzie musiał wejść do aplikacji i nacisnąć Pause, jeśli chce zadzwonić. Dlatego potrzebujemy sposobu aby poinformowania system Android, że ​​nasza aplikacja jest odtwarzaczem, może być kontrolowana i że w tej chwili gramy taki i taki utwór z takiego i tego albumu. W tym momencie właśnie pojawia się MediaSession i MediaController wprowadzony w wersji Lollipop (API 21). Nieco później w bibliotece wsparcia pojawili się ich bliźniacy MediaSessionCompat i MediaControllerCompat.

1. Czym jest MediaSession?

MediaSession – to interfejs pośredniczące między Twoją aplikacją a innymi urządzeniami (Android TV, Android Wear, Android Auto, przyciski multimedialne itp.), które mogą kontrolować aplikacje multimedialnie. Czyli to oprogramowanie, które pozwala na kontrolowanie multimediów w Twojej aplikacji poza interfejsem użytkownika aplikacji. MediaSession to nie odtwarzacz multimedialny taki jak MediaPlayer, Aby dokładniej to zrozumieć, spójrz na poniższy diagram:

Jeżeli użytkownik naciśnie przycisk pauzy na słuchawkach to system wyszukuje, która aplikacja ostatnio miała aktywną sesję multimedialną, Następnie ta aplikacja za pomocą MediaSession odbiera to działanie i wywołuje metodą onPause(), a ta metoda przesyła sygnał do Twojego odtwarzacza, który zatrzymuje dźwięk. Warto też poinformować z powrotem MediaSession, że zatrzymałeś dźwięk. wywołując setPlaybackState() z nowym stanem odtwarzania (w tym przypadku, utwór jest spauzowany), a jednocześnie też informujesz systemu Android. Interfejs użytkownika Twojej aplikacji powinien również łączyć się z MediaSession w celu wywołania onPlaybackStateChanged() w celu poinformowania UI aplikacji, że stan odtwarzania się zmienił – zmiana ikonki z Play na Pause.

2. Do czego służy MediaSession?

MediaSession najczęściej jest używana w aplikacjach do odtwarzania muzyki, a interfejs nie zawsze znajduje się na pierwszym planie. Możesz też użyć to w aplikacjach, które wyświetlają wideo. MediaSession wykonuje następujące czynności:

    • sterowaniem odtwarzaniem -  zapewnia pojedynczy interfejs do sterowania odtwarzaniem. Możesz korzystać z wielu podmiotów aby kontrolować stan odtwarzania, np.pauza, zatrzymanie, przejście do następnego utworu. Nie musisz tworzyć dla każdego urządzenia specjalnego kodu, który będzie kontrolował ten stan.

 

  • synchronizacja stanu  - nadaje bieżący stan odtwarzania (odtwarzanie, wstrzymanie, zatrzymanie itp.). Dodatkowo możesz uzyskać metadane mediów (okładka albumu, czas trwania utworu, tytuł utworu itp.). Dzięki temu zabiegowi wszystkie urządzenia które są połączone z Twoją aplikacją mogą dostawać takie informacje.

3. Jak tego używać?

Poniżej przedstawię kod, dzięki któremu będziesz mógł kontrolować MediaPlayer za pomocą telefonu + dodatkowego urządzenia podłączonego do sesji medialnej (w moim przypadku będzie to zegarek Garmin).
Na samym początku s stworzymy usługę z MediaSession i MediaPlayer. W AndroidManifest.xml dodajemy wpis:

<service
    android:name=".Service.PlayerService"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_BUTTON" />
    </intent-filter>
</service>

Wpis intent-filter informuje system, że potrzebujemy odbierać zdarzenia z przycisków multimedialnych. Tworzony naszą klasę i rozszerzamy o Service. Metoda onCreate() może wyglądać tak:

@Override
public void onCreate() {
    super.onCreate();
    initMediaSession();
    initMediaPlayer();
}

Metoda initMediaSession():

private void initMediaSession() {
    mediaSession = new MediaSessionCompat(this, "PlayerService");
    mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
    mediaSession.setCallback(mediaSessionCallback);
}

Tworzymy instancję MediaSession i nadajemy odpowiednie flagi. Przypisujemy callbacka, a on może wyglądać tak:

MediaSessionCompat.Callback mediaSessionCallback = new MediaSessionCompat.Callback() {
        @Override
        public void onPlay() {
           //mediaSession.setActive(true);
		   mediaSession.setPlaybackState(
                    stateBuilder.setState(PlaybackStateCompat.STATE_PLAYING,
										  PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1)
                    .build());
           mediaPlayer.start();
		   updateMetadata();
        }
        @Override
        public void onPause() {
            mediaPlayer.pause();
			mediaSession.setPlaybackState(
                    stateBuilder.setState(PlaybackStateCompat.STATE_PAUSED,
										  PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1)
					.build());
			updateMetadata();
        }
        @Override
        public void onStop() {
            mediaSession.setActive(false);
            mediaPlayer.stop();
			mediaSession.setPlaybackState(
                    stateBuilder.setState(PlaybackStateCompat.STATE_STOPPED,
										  PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1)
					.build());
			updateMetadata();
        }
    };

W metodzie onPlay() aktywujemy MediaSession (tymczasowo komentarz), a w metodzie onStop() dezaktywujemy. Tutaj podałem Ci tylko 3 przykładowe przyciski. Jeżeli będziesz robił aplikację pamiętaj aby zaimplementować więcej przycisków takich jak, następny/poprzedni utwór, zmiana głośności. Zastanawiasz się też pewnie jak obsłużyć długie przyciśnięcie przycisku oraz podwójny klik. W tym celu musisz nadpisać metodę MediaSession.Callback.onMediaButtonEvent() Przykładowy kod możesz znaleźć tutaj.
Wracając do naszego kodu, również informujemy system o stanie odtwarzania – setPlaybackState() i aktualizujemy metadane o utworze – updateMetadata(). A ta metoda może wyglądać tak:

private void updateMetadataFromTrack() {
    metadataBuilder.putString(MediaMetadataCompat.,"Hello ");
    metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE,"Hello ");
    metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "First ");
    metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST,"My Env ");
    metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
    mediaSession.setMetadata(metadataBuilder.build());
}

Jeszcze została nam metoda initMediaPlayer():

private void initMediaPlayer() {
    String url = "https://....";
    mediaPlayer = new MediaPlayer();
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    try {
        mediaPlayer.setDataSource(url);
        mediaPlayer.prepare();
    } catch (IOException e) {
        e.printStackTrace();
    }
    mediaSession.setActive(true);
    mediaSession.setPlaybackState(
            stateBuilder.setState(PlaybackStateCompat.STATE_PLAYING,
                                  PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1)
                    .build());
    updateMetadataFromTrack();
    mediaPlayer.start();
}

Pomijam tutaj pełną implementację tego odtwarzacza, ponieważ do przykładu nie jest to nam potrzebne. Czyli brakuje tutaj onError(), onPrepared() itd. Pamiętaj, że musisz też napisać kod, który będzie informował sesję medialną  o zakończeniu utworu czy przejściu na następny utwór.
Jeszcze zostało nam wystartowanie usługi w aktywności:

Intent intent = new Intent(this, PlayerService.class);
startService(intent);

Działa? Na pewno. Podłącz słuchawki i sprawdź. A jak sterować za pomocą innego urządzenia, np.: zegarka? Wystarczy dostać uprawnienia do powiadomień systemu Android. W aktywności dodajmy i wywołujemy metodę:

private void notificationAccessPermission() {
    if (!Settings.Secure.getString(this.getContentResolver(), "enabled_notification_listeners").contains(this.getPackageName())) {
        Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
        startActivity(intent);
    }
}

Jak już masz dostęp do powiadomień, możemy również odczytywać dane z sesji multimedialnej (niekoniecznie stworzoną przez Twoją aplikację):

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void getMediaSession(){
    MediaSessionManager mediaSessionManager = (MediaSessionManager) getSystemService(Context.MEDIA_SESSION_SERVICE);
    List<MediaController> mediaControllerList = mediaSessionManager.getActiveSessions(
            new ComponentName(this, NotificationListener.class));
    Log.i("TAG", "found " + mediaControllerList.size() + " controllers");
    MediaMetadata meta = mediaControllerList.get(0).getMetadata();
    String artist = meta.getString(MediaMetadata.METADATA_KEY_ARTIST);
    String title = meta.getString(MediaMetadata.METADATA_KEY_TITLE);
    Log.i("TAG", artist + " - "+title );
    // Press Pauze
    mediaControllerList.get(0).getTransportControls().pause();
}

Jak widzisz, możesz odczytać dane o utworze i sterować też odtwarzaniem obcej aplikacji. Tak dobrze rozumiesz podłączysz się do innej aplikacji, która korzysta z MediaSession. Uważaj! Może być kilka sesji aktywnych i musisz wybrać z której chcesz korzystać.
Pamiętaj dodać wpis do AndroidMenifest.xml:

<service android:name=".Service.NotificationListener"
    android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
    android:enabled="true" android:exported="true">
    <intent-filter>
        <action android:name="android.service.notification.NotificationListenerService" />
    </intent-filter>
</service>

oraz utworzeniu klasy:

public class NotificationListener extends NotificationListenerService {
    public NotificationListener() {
    }
}

4. Sterowanie za pomocą przycisków UI.

Ok, mamy już aktywną usługę, która sobie gra. Zmodyfikujmy naszą aktywność w taki sposób, aby muzyka odtwarzała się po naciśnięciu play, a nie zaraz po uruchomieniu aktywności. Nasza aktywność może wyglądać tak:

private PlayerService.PlayerServiceBinder playerServiceBinder;
private MediaControllerCompat mediaController;
private MediaControllerCompat.Callback callback;
private ServiceConnection serviceConnection;
// Media Controls
private Button playButton;
private Button pauseButton;
private Button stopButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_media_controller);
    playButton = findViewById(R.id.play);
    pauseButton = findViewById(R.id.pause);
    stopButton = findViewById(R.id.stop);
    playButton.setOnClickListener(this);
    pauseButton.setOnClickListener(this);
    stopButton.setOnClickListener(this);
    initMediaController();
}
private void initMediaController(){
    callback = new MediaControllerCompat.Callback() {
        @Override
        public void onPlaybackStateChanged(PlaybackStateCompat state) {
            if (state == null) return;
            boolean playing = state.getState() == PlaybackStateCompat.STATE_PLAYING;
            playButton.setEnabled(!playing);
            pauseButton.setEnabled(playing);
            stopButton.setEnabled(playing);
        }
    };
    serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            playerServiceBinder = (PlayerService.PlayerServiceBinder) service;
            try {
                mediaController = new MediaControllerCompat(MediaControllersActivity.this, playerServiceBinder.getMediaSessionToken());
                mediaController.registerCallback(callback);
                callback.onPlaybackStateChanged(mediaController.getPlaybackState());
            }
            catch (RemoteException e) {
                mediaController = null;
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            playerServiceBinder = null;
            if (mediaController != null) {
                mediaController.unregisterCallback(callback);
                mediaController = null;
            }
        }
    };
    bindService(new Intent(this, PlayerService.class), serviceConnection, BIND_AUTO_CREATE);
}

W klasie PlayerService dodajemy:

@Override
public IBinder onBind(Intent intent) {
    return new PlayerServiceBinder();
}
public class PlayerServiceBinder extends Binder {
    public MediaSessionCompat.Token getMediaSessionToken() {
        return mediaSession.getSessionToken();
    }
}

Teraz gdy naciśniemy na telefonie przycisk play, muzyka zacznie grać. W zależności od stanu odtwarzania, poszczególne przyciski będą aktywne. Jeśli odtwarzana jest muzyka to przycisk play jest nieaktywny, a przyciski pause i stop są aktywne.
Jeszcze została nam mała modyfikacja. Usuń z metody initMediaPlayer ten wpis:

mediaSession.setActive(true);
        mediaSession.setPlaybackState(
                stateBuilder.setState(PlaybackStateCompat.STATE_PLAYING,
                        PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1)
                        .build());
        updateMetadataFromTrack();
        mediaPlayer.start();

A w metodzie onPlay() odkomentuj aktywowanie sesji medialnej. Teraz masz sterowanie za pomocą UI oraz innego urządzenia.

5. Wparcie dla APi <21.

Jeżeli Twoja aplikacja ma działać na API<21 musisz dodać wsparcie. W AndroidManifest.xml dodaj taki wpis:

<receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_BUTTON" />
    </intent-filter>
</receiver>

Jeśli odpowiednia usługa, która odpowiada za odbieranie sygnałów z przycisków nie zostanie znaleziona lub jest ich kilka, zostanie zgłoszony wyjątek IllegalStateException. Teraz dodaj do usługi taki wpis:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    MediaButtonReceiver.handleIntent(mediaSession, intent);
    return super.onStartCommand(intent, flags, startId);
}

Metoda handleIntent() analizuje kody przycisków z intencji i wywołuje odpowiednie wywołania zwrotne w MediaSession.
W systemach z API>=21, system uzyskuje bezpośrednio dostęp do MediaSession, nie używa rozgłaszaczy do zdarzeń odbieranych z przycisków. Jeśli jednak nasza sesja multimedialna jest nieaktywna – setActive(false), i chcesz ją aktywować skorzystaj z poniższego kodu. W klasie z usługą, w metodzie onCreate() dodaj:

Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null, appContext, MediaButtonReceiver.class);
mediaSession.setMediaButtonReceiver(PendingIntent.getBroadcast(appContext, 0, mediaButtonIntent, 0));

W systemach z API<21, metoda setMediaButtonReceiver nie robi nic.

6. Podsumowanie

MediaSession to interfejs, który łączy z jednej strony Twój odtwarzacz audio lub video, z drugiej strony mamy urządzenia lub kontrolki, za pomocą których możemy kontrolować i odczytywać dane o sesji medialnej oraz informujemy system co aktualnie aplikacja robi. Pamiętaj  że MediaSession nie ma nic wspólnego z reprodukcją dźwięku, chodzi tylko o kontrolowanie odtwarzacza i jego metadanych. Po przeczytaniu tego wpisu wiesz w jaki sposób stworzyć sesję medialną oraz jak do niej się podłączyć i nią zarządzać.

Co dalej?

  • Polub stronę MYENV na Facebooku oraz śledź mnie na Twitterze
  • Zachęcam do komentowania i pisania propozycji tematów, o których chcesz przeczytać
  • Poleć ten wpis za pomocą poniższych przycisków. Będę Ci za to bardzo wdzięczny 🙂
  • Życzę Ci miłego dnia i miłego kodowania 🙂