niedziela, 3 kwietnia 2016

[DSP2016] Android prosto z poligonu odc.5 (odtwarzanie audio - pierwsze podejście)

Wracam do pisania w ramach DSP. W ciągu kilku dni powstały dwie wstępne wersje aplikacji z odtwarzaniem audio. Czas na jakieś przemyślenia i podsumowania. Temat  okazał się dość złożony i nie prosty, jeśli chcemy cieszyć się w pełni profesjonalnym odtwarzaniem w tle. Ale po kolei. Dziś zrobię wprowadzenie w problematykę odtwarzania audio i opiszę dość prostą pierwszą wersję kodu, której rozwój już zarzuciłem (ale zamroziłem w branchu). Obecnie wykonałem już większą część prac nad drugą znacznie bardziej profesjonalną wersją, ale o niej następnym razem.

image image

Podejściem najbardziej na skróty jest wywołanie “systemowego” odtwarzacza audio. Dla testu zrobiłem to za pomocą kilku linii:

Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
File audioFile = new File(mediafileItem.filePath);
intent.setDataAndType(Uri.fromFile(audioFile), mediafileItem.mimeType);
startActivity(intent);

Obiekt mediafileItem to mój obiekt przechowujący m.in ścieżkę do pliku czy jego rodzaj.

Trudno jednak byśmy się zadowolili takim odtwarzaniem.  Generalnie do odtwarzania audio w swojej aplikacji używamy w Androidzie czegoś takiego jak MediaPlayer. Jego użycie w aktywności jest proste. Jednak jest tutaj pewna niedogodność. Obracamy telefon i … dźwięk nam się urywa. Jak wiemy da się to wytłumaczyć budowaniem na nowo aktywności przy zmianie orientacji ekranu, ale nie jest to przyjazne dla nas czy innych userów. Co robić?

Możemy zaimplementować odtwarzanie audio w tle w ramach serwisu. Jak ktoś zna platformę mobilną/uniwersalną Windows, to tam też jest taka funkcjonalność. W moim odczuciu w Androidzie mamy większą dowolność implementacji, możemy sami napisać sobie taki stosunkowo prosty serwis z MediaPlayer od zera, można też wbić się w bardziej profesjonalną infrastrukturę (ale nadal z jawnym używaniem MediaPlayer), jak jest czynione w dwóch obszernych, częściowo zazębiających się samplach z Android SDK ( MediaBrowserService i Universal Android Music Player).

Zróbmy sobie w ramach ćwiczenia trzy przyciski play, pause i stop w aktywności startowej, które wydają polecenia serwisowi w tle. Wzoruję się w jakimś stopniu na przykładzie z książki, o której wspominałem wcześniej. Zacznijmy od serwisu:

public class  BackgroundAudioService  extends Service implements MediaPlayer.OnCompletionListener {

MediaPlayer mediaPlayer;
String filePath;
    public class BackgroundAudioServiceBinder extends Binder {
BackgroundAudioService getService() {
return BackgroundAudioService.this; }
}
    private final IBinder basBinder = new BackgroundAudioServiceBinder();
    @Nullable
@Override
public IBinder onBind(Intent intent) {
return basBinder;
}
    @Override
public void onCreate() {
}
    @Override
public int onStartCommand(Intent intent, int flags, int startId) {

if (mediaPlayer == null)
{
if (filePath == null && intent != null)
filePath = intent.getStringExtra(MusicHelper.MEDIA_FILE_PATH);

if (filePath == null)
return START_STICKY;

mediaPlayer = MediaPlayer.create(this, Uri.parse(filePath));
mediaPlayer.setOnCompletionListener(this);
}

if (!mediaPlayer.isPlaying()) {
mediaPlayer.start();
}

return START_STICKY;
}
    public void onDestroy() {
filePath = null;
if (mediaPlayer != null) {

if (mediaPlayer.isPlaying())
mediaPlayer.stop();

mediaPlayer.release();
mediaPlayer = null;
}
}
    public void pause() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.pause();
}
}
    @Override
public void onCompletion(MediaPlayer mp) {
stopSelf();
}
}

Serwis odtwarza tylko jeden utwór, po czym kończy swoją pracę. Należy zwrócić uwagę na klasę BackgroundAudioServiceBinder, której używamy do implementacji dostarczającej referencji do obiektu serwisu, tak by można jej było użyć z poziomu aktywności i wywołać np. metodę pause. Serwis oczywiście standardowo rejestrujemy w manifeście aplikacji poprzez wpis:

<service android:name=".BackgroundAudioService" />

Przejdźmy teraz do aktywności z przyciskami sterującymi. U mnie wyglądało to w skrócie następująco:

public class MainActivity extends BaseActivity {

Button startButton;
Button stopButton;
Button pauseButton;
    Intent serviceIntent;
String filePath;
private BackgroundAudioService baService;
    private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder baBinder) {
baService = ((BackgroundAudioService.BackgroundAudioServiceBinder)baBinder).getService();
}

public void onServiceDisconnected(ComponentName className) {
baService = null;
}
};
    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

startButton = (Button) this.findViewById(R.id.StartButton);
stopButton = (Button) this.findViewById(R.id.StopButton);
pauseButton = (Button) this.findViewById(R.id.PauseButton);
startButton.setOnClickListener(this);
stopButton.setOnClickListener(this);
pauseButton.setOnClickListener(this);
        serviceIntent = new Intent(this, BackgroundAudioService.class);

      if (filePath != null)
serviceIntent.putExtra(MusicHelper.MEDIA_FILE_PATH, filePath);
    } 

@Override
public void onClick(View v) {
if (v == startButton) {
startService(serviceIntent);
bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
} else if (v == stopButton) {
stopService(serviceIntent);
unbindService(serviceConnection);
}
else if (v == pauseButton) {
baService.pause();
}
}

}

Kod ten faktycznie odpali nam odtwarzanie muzyki w tle, którą możemy sobie zatrzymać lub spauzować. Całość działa całkiem dobrze, aczkolwiek pewne zszycia naokoło trochę mogą komplikować, podczas dokładnego testowania w różnych scenariuszach czasami aplikacja mogła przestać działać, co prawda stosunkowo rzadko, ale jednak. Czasami mogły zdarzyć mi się też pewne zachowania niepożądane. Wyeliminowałem kilka bugów, ale czasami i tak coś wymagałoby jeszcze stabilizacji, ale to sobie już odpuściłem, bo do pełnego odtwarzacza jednak w tym wszystkim bardzo daleko. Gdzie mamy informowanie na bieżaco o stanie odtwarzania? Brakuje mi tu jakiegoś wygodnego gotowego sprzężenia między UI a serwisem. Poza tym nie uwzględniamy wiele aspektów, które w profesjonalnej implementacji powinny być uwzględnione np. zapewnienie odtwarzania w czasie uśpienia całego urządzenia, obsługę audio focusa (przełączanie z innymi żródłami dżwięku w systemie lub mocne przyciszanie np. gdy ktoś do nas dzwoni). O tym wszystkim warto poczytać w dokumentacji na stronach http://developer.android.com/guide/topics/media/mediaplayer.html i http://developer.android.com/training/managing-audio/index.html.

Stosując podejście z http://developer.android.com/training/auto/audio/index.html i wyżej wspomniane sample z SDK można stworzyć mocno uniwersalne rozwiązanie z uwzględniem wszystkich wymienionych poprzednio postulatów ze wsparciem dla Android TV, Auto, Wear i castowaniem odtwarzania na zewnętrzne urządzenia. Tą tematyką zajmiemy się następnym razem.

Brak komentarzy: