Deep Dive: Най-добрите практики на MediaPlayer

Снимка на Марсела Ласкоски на Unsplash

Изглежда MediaPlayer е измамно прост за използване, но сложността живее точно под повърхността. Например, може да е изкушаващо да напишете нещо подобно:

MediaPlayer.create (контекст, R.raw.cowbell) .start ()

Това работи добре първия и вероятно вторият, третия или дори повече пъти. Всеки нов MediaPlayer обаче консумира системни ресурси, като памет и кодеци. Това може да влоши ефективността на приложението ви и вероятно на цялото устройство.

За щастие е възможно да използвате MediaPlayer по начин, който е едновременно прост и безопасен, като следвате няколко прости правила.

Простият случай

Най-основният случай е, че имаме звуков файл, може би суров ресурс, който просто искаме да играем. В този случай ще създадем един играч, който ще го използваме повторно всеки път, когато трябва да пуснем звук. Плейърът трябва да бъде създаден с нещо подобно:

private val mediaPlayer = MediaPlayer (), приложи {
    setOnPreparedListener {start ()}
    setOnCompletionListener {reset ()}
}

Плейърът е създаден с двама слушатели:

  • OnPreparedListener, който автоматично ще започне възпроизвеждане, след като плейърът е подготвен.
  • OnCompletionListener, който автоматично изчиства ресурсите, когато възпроизвеждането приключи.

Със създадения плейър следващата стъпка е да се направи функция, която приема идентификатор на ресурс и използва този MediaPlayer, за да го възпроизведе:

замени забавно playSound (@RawRes rawResId: Int) {
    val imovFileDescriptor = context.resources.openRawResourceFd (rawResId)?: връщане
    mediaPlayer.run {
        нулиране()
        setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength)
        prepareAsync ()
    }
}

В този кратък метод се случва доста малко:

  • Идентификационният номер на ресурса трябва да бъде преобразуван в AssetFileDescriptor, защото това MediaPlayer използва за възпроизвеждане на сурови ресурси. Проверката на нула гарантира, че ресурсът съществува.
  • Повторното повикване () гарантира, че играчът е в инициализирано състояние. Това работи независимо в какво състояние е играчът.
  • Задайте източника на данни за плейъра.
  • PrepaAsync подготвя плейъра за игра и се връща незабавно, поддържайки потребителския интерфейс отзивчив. Това работи, защото прикаченият OnPreparedListener започва да играе след като източникът е подготвен.

Важно е да се отбележи, че не призоваваме освобождаване () на нашия плейър или не го настройваме на нула. Искаме да го използваме отново! Затова вместо това наричаме reset (), което освобождава паметта и кодеците, които е използвал.

Възпроизвеждането на звук е толкова просто, колкото и да се обадите:

playSound (R.raw.cowbell)

Simple!

Още каубойци

Възпроизвеждането на един звук в даден момент е лесно, но какво ще стане, ако искате да пуснете друг звук, докато първият все още се възпроизвежда? Извикването на playSound () няколко пъти като това няма да работи:

playSound (R.raw.big_cowbell)
playSound (R.raw.small_cowbell)

В този случай R.raw.big_cowbell започва да се подготвя, но вторият разговор нулира играча преди всичко да се случи, така че само вие чувате R.raw.small_cowbell.

А какво ще стане, ако искахме да изсвирим едновременно няколко звука? За всяко от тях трябва да създадем MediaPlayer Най-простият начин да направите това е да имате списък с активни играчи. Може би нещо подобно:

клас MediaPlayers (контекст: контекст) {
    private val контекст: Context = context.applicationContext
    частни val играчиInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer (), приложи {
        setOnPreparedListener {start ()}
        setOnCompletionListener {
            it.release ()
            playerInUse - = то
        }
    }

    замени забавно playSound (@RawRes rawResId: Int) {
        val imovFileDescriptor = context.resources.openRawResourceFd (rawResId)?: връщане
        val mediaPlayer = buildPlayer ()

        mediaPlayer.run {
            playerInUse + = it
            setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset,
                    assetFileDescriptor.declaredLength)
            prepareAsync ()
        }
    }
}

Сега, когато всеки звук има свой плейър, е възможно да свирите заедно R.raw.big_cowbell и R.raw.small_cowbell! Чудесно!

... Е, почти перфектно. В нашия код няма нищо, което ограничава броя на звуците, които могат да се възпроизвеждат наведнъж, и MediaPlayer все още трябва да има памет и кодеци, с които да работи. Когато те изчерпват, MediaPlayer се проваля безшумно, като само в logcat отбелязва „E / MediaPlayer: Грешка (1, -19)“.

Въведете MediaPlayerPool

Искаме да подкрепим възпроизвеждането на няколко звука наведнъж, но не искаме да изтече памет или кодеци. Най-добрият начин да управляваме тези неща е да имаме пул от плейъри и след това да изберете един, който да използваме, когато искаме да пуснем звук. Можем да актуализираме кода си така:

клас MediaPlayerPool (контекст: контекст, maxStreams: Int) {
    private val контекст: Context = context.applicationContext

    private val mediaPlayerPool = mutableListOf  () .also {
        за (i in 0..maxStreams) it + = buildPlayer ()
    }
    частни val играчиInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer (), приложи {
        setOnPreparedListener {start ()}
        setOnCompletionListener {recyclePlayer (it)}
    }

    / **
     * Връща [MediaPlayer], ако има такъв,
     * в противен случай е нула.
     * /
    частна заявка за забавлениеPlayer (): MediaPlayer? {
        върнете, ако (! mediaPlayerPool.isEmpty ()) {
            mediaPlayerPool.removeAt (0) .also {
                playerInUse + = it
            }
        } else null
    }

    частно забавно recyclePlayer (mediaPlayer: MediaPlayer) {
        mediaPlayer.reset ()
        playerInUse - = mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }

    fun playSound (@RawRes rawResId: Int) {
        val imovFileDescriptor = context.resources.openRawResourceFd (rawResId)?: връщане
        val mediaPlayer = requestPlayer ()?: връщане

        mediaPlayer.run {
            setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset,
                    assetFileDescriptor.declaredLength)
            prepareAsync ()
        }
    }
}

Сега няколко звука могат да възпроизвеждат наведнъж, а ние можем да контролираме максималния брой едновременни плейъри, за да не използваме твърде много памет или твърде много кодеци. И тъй като рециклираме инстанциите, събирачът на боклук няма да трябва да работи, за да почисти всички стари екземпляри, които са приключили.

Има няколко недостатъка на този подход:

  • След възпроизвеждане на звуци от maxStreams, всички допълнителни обаждания към playSound се игнорират, докато играчът не бъде освободен. Можете да заобиколите това, като „откраднете“ плейър, който вече се използва за възпроизвеждане на нов звук.
  • Може да има значително изоставане между извикване на playSound и действително възпроизвеждане на звука. Въпреки че MediaPlayer се използва повторно, това всъщност е тънка обвивка, която контролира основен C ++ естествен обект чрез JNI. Родният плейър се унищожава всеки път, когато се обадите на MediaPlayer.reset (), и трябва да бъде пресъздаден, когато MediaPlayer е подготвен.

Подобряването на латентността при запазване на способността за повторно използване на играчите е по-трудно да се направи. За щастие, за някои видове звуци и приложения, където се изисква ниска латентност, има друга опция, която ще разгледаме следващия път: SoundPool.