Deep Dive: Кращі практики MediaPlayer

Фото Марсели Ласкоскі на Unsplash

MediaPlayer здається оманливим простим у використанні, але складність живе трохи нижче поверхні. Наприклад, може бути спокусливо написати щось подібне:

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

Це чудово спрацьовує перший та, мабуть, другий, третій чи навіть більше разів. Однак кожен новий MediaPlayer споживає системні ресурси, такі як пам'ять та кодеки. Це може погіршити продуктивність вашої програми та, можливо, всього пристрою.

На щастя, користуватися MediaPlayer можна просто і безпечно, дотримуючись кількох простих правил.

Простий випадок

Найбільш основний випадок - це те, що у нас є звуковий файл, можливо, сирий ресурс, який ми просто хочемо відтворити. У цьому випадку ми створимо одного гравця повторно використовувати його кожен раз, коли нам потрібно відтворити звук. Гравець повинен бути створений приблизно так:

приватний val mediaPlayer = MediaPlayer () застосовується {
    setOnPreparedListener {start ()}
    setOnCompletionListener {reset ()}
}

Програвач створений з двома слухачами:

  • OnPreparedListener, який автоматично розпочне відтворення після того, як програвач буде підготовлений.
  • OnCompletionListener, який автоматично очищає ресурси після закінчення відтворення.

Після створення програвача наступним кроком є ​​створення функції, яка приймає ідентифікатор ресурсу і використовує цей MediaPlayer для його відтворення:

переосмислити весело playSound (@RawRes rawResId: Int) {
    val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: повернення
    mediaPlayer.run {
        скинути ()
        setDataSource (resourceFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength)
        prepAsync ()
    }
}

У цьому короткому методі трапляється зовсім небагато:

  • Ідентифікатор ресурсу повинен бути перетворений в AssetFileDescriptor, оскільки саме це MediaPlayer використовує для відтворення сировинних ресурсів. Нульова перевірка забезпечує наявність ресурсу.
  • Виклик скидання () гарантує, що гравець перебуває в ініціалізованому стані. Це працює незалежно від того, в якому стані знаходиться гравець.
  • Встановіть джерело даних для програвача.
  • PrepaAsync готує гравця до гри та повертається негайно, підтримуючи інтерфейс користувача чутливим. Це працює, тому що доданий OnPreparedListener починає програвати після того, як джерело підготовлено.

Важливо зауважити, що ми не називаємо випуск () на плеєрі або не встановлюємо його на нуль Ми хочемо його повторно використовувати! Тому замість цього ми викликаємо reset (), що звільняє пам’ять та кодеки, якими він користувався.

Відтворення звуку так само просто, як дзвінки:

playSound (R.raw.cowbell)

Просто!

Більше Каубелів

Відтворювати один звук за раз легко, але що робити, якщо ви хочете запустити інший звук, поки перший все ще грає? Виклик playSound () декілька разів, як це не працює:

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

У цьому випадку R.raw.big_cowbell починає готуватися, але другий дзвінок скидає програвача до того, як щось може статися, тому ви чуєте лише R.raw.small_cowbell.

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

клас MediaPlayers (контекст: контекст) {
    приватний вал контекст: контекст = контекст.аплікаціяContext
    приватний val playerInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer () застосувати {
        setOnPreparedListener {start ()}
        setOnCompletionListener {
            it.release ()
            playerInUse - = це
        }
    }

    переосмислити весело playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: повернення
        val mediaPlayer = buildPlayer ()

        mediaPlayer.run {
            playerInUse + = це
            setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset,
                    assetFileDescriptor.declaredLength)
            prepAsync ()
        }
    }
}

Тепер, коли у кожного звуку є власний програвач, можна грати як R.raw.big_cowbell, так і R.raw.small_cowbell! Ідеально!

… Ну, майже ідеально. У нашому коді немає нічого, що обмежує кількість звуків, які можуть відтворюватись одразу, і MediaPlayer все ще повинен мати пам'ять та кодеки для роботи. Коли їх закінчується, MediaPlayer виходить з ладу мовчки, лише зазначаючи "E / MediaPlayer: Помилка (1, -19)" у logcat.

Введіть MediaPlayerPool

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

клас MediaPlayerPool (контекст: контекст, maxStreams: Int) {
    приватний вал контекст: контекст = контекст.аплікаціяContext

    приватний val mediaPlayerPool = mutableListOf  () .also {
        для (я в 0..maxStreams) це + = buildPlayer ()
    }
    приватний val playerInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer () застосувати {
        setOnPreparedListener {start ()}
        setOnCompletionListener {recyclePlayer (це)}
    }

    / **
     * Повертає [MediaPlayer], якщо такий доступний,
     * інакше недійсне.
     * /
    приватний запит на розвагиPlayer (): MediaPlayer? {
        повернути, якщо (! mediaPlayerPool.isEmpty ()) {
            mediaPlayerPool.removeAt (0) .also {
                playerInUse + = це
            }
        } else null
    }

    приватне весело recyclePlayer (mediaPlayer: MediaPlayer) {
        mediaPlayer.reset ()
        playerInUse - = mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }

    fun playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: повернення
        val mediaPlayer = requestPlayer ()?: повернення

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

Зараз можна відтворювати декілька звуків одночасно, і ми можемо контролювати максимальну кількість одночасних плеєрів, щоб уникнути використання занадто багато пам'яті або занадто багато кодеків. А оскільки ми переробляємо екземпляри, збирач сміття не повинен буде працювати для очищення всіх старих примірників, які закінчили програвати.

У цього підходу є кілька недоліків:

  • Після відтворення звуків maxStreams будь-які додаткові дзвінки в playSound ігноруються, поки гравець не звільниться. Ви можете обійти це, "вкравши" програвач, який вже використовується для відтворення нового звуку.
  • Між викликом playSound і відтворенням звуку може бути значне відставання. Навіть незважаючи на те, що MediaPlayer використовується повторно, це фактично тонка обгортка, яка керує базовим об'єктом C ++ за допомогою JNI. Народний програвач знищується щоразу, коли ви викликаєте MediaPlayer.reset (), і його потрібно відтворити кожен раз, коли програма MediaPlayer готується.

Поліпшити затримку при збереженні можливості повторного використання гравців важче зробити. На щастя, для певних типів звуків і програм, де потрібна низька затримка, є ще один варіант, який ми розглянемо наступного разу: SoundPool.