Deep Dive: лучшие практики MediaPlayer

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

MediaPlayer, кажется, обманчиво прост в использовании, но сложность живет чуть ниже поверхности. Например, может быть заманчиво написать что-то вроде этого:

MediaPlayer.create (context, R.raw.cowbell) .start ()

Это прекрасно работает первый и, вероятно, второй, третий или даже больше раз. Однако каждый новый MediaPlayer потребляет системные ресурсы, такие как память и кодеки. Это может ухудшить производительность вашего приложения и, возможно, всего устройства.

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

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

Самый простой случай - у нас есть звуковой файл, возможно, необработанный ресурс, который мы просто хотим воспроизвести. В этом случае мы создадим одного игрока и будем использовать его каждый раз, когда нам нужно будет воспроизвести звук. Плеер должен быть создан примерно так:

private val mediaPlayer = MediaPlayer (). apply {
    setOnPreparedListener {start ()}
    setOnCompletionListener {reset ()}
}

Плеер создается с двумя слушателями:

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

После создания проигрывателя следующим шагом является создание функции, которая берет идентификатор ресурса и использует этот MediaPlayer для его воспроизведения:

переопределить забавный playSound (@RawRes rawResId: Int) {
    val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: return
    mediaPlayer.run {
        сброс()
        setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength)
        prepareAsync ()
    }
}

В этом коротком методе происходит довольно много:

  • Идентификатор ресурса должен быть преобразован в AssetFileDescriptor, потому что это то, что MediaPlayer использует для воспроизведения необработанных ресурсов. Нулевая проверка гарантирует, что ресурс существует.
  • Вызов reset () гарантирует, что игрок находится в состоянии инициализации. Это работает независимо от того, в каком состоянии находится игрок.
  • Установите источник данных для игрока.
  • prepareAsync подготавливает игрока к игре и сразу же возвращается, сохраняя отзывчивость интерфейса. Это работает, потому что присоединенный OnPreparedListener начинает воспроизводиться после подготовки источника.

Важно отметить, что мы не вызываем release () на нашем плеере и не устанавливаем его на нуль. Мы хотим использовать это снова! Поэтому вместо этого мы вызываем метод 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 для каждого. Самый простой способ сделать это - иметь список активных игроков. Возможно, что-то вроде этого:

class MediaPlayers (context: Context) {
    закрытый val context: Context = context.applicationContext
    private val PlayersInUse = mutableListOf  ()

    личное развлечение buildPlayer () = MediaPlayer (). apply {
        setOnPreparedListener {start ()}
        setOnCompletionListener {
            it.release ()
            PlayersInUse - = это
        }
    }

    переопределить забавный playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: return
        val mediaPlayer = buildPlayer ()

        mediaPlayer.run {
            PlayersInUse + = это
            setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset,
                    assetFileDescriptor.declaredLength)
            prepareAsync ()
        }
    }
}

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

… Ну, почти идеально. В нашем коде нет ничего, что ограничивало бы количество звуков, которые могут воспроизводиться одновременно, и MediaPlayer все еще должен иметь память и кодеки для работы. Когда они заканчиваются, MediaPlayer молча завершается сбоем, отмечая только «E / MediaPlayer: Error (1, -19)» в logcat.

Введите MediaPlayerPool

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

class MediaPlayerPool (context: Context, maxStreams: Int) {
    закрытый val context: Context = context.applicationContext

    private val mediaPlayerPool = mutableListOf  (). также {
        для (я в 0..maxStreams) это + = buildPlayer ()
    }
    private val PlayersInUse = mutableListOf  ()

    личное развлечение buildPlayer () = MediaPlayer (). apply {
        setOnPreparedListener {start ()}
        setOnCompletionListener {recyclePlayer (it)}
    }

    / **
     * Возвращает [MediaPlayer], если он доступен,
     * иначе ноль.
     * /
    частное удовольствие requestPlayer (): MediaPlayer? {
        вернуть if (! mediaPlayerPool.isEmpty ()) {
            mediaPlayerPool.removeAt (0) .also {
                PlayersInUse + = это
            }
        } еще ноль
    }

    личное развлечение recyclePlayer (mediaPlayer: MediaPlayer) {
        mediaPlayer.reset ()
        PlayersInUse - = mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }

    забавный playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: return
        val mediaPlayer = requestPlayer ()?: return

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

Теперь несколько звуков могут воспроизводиться одновременно, и мы можем контролировать максимальное количество одновременных проигрывателей, чтобы не использовать слишком много памяти или слишком много кодеков. И, поскольку мы перерабатываем экземпляры, сборщику мусора не нужно бежать, чтобы очистить все старые экземпляры, которые закончили играть.

У этого подхода есть несколько недостатков:

  • После воспроизведения звуков maxStreams любые дополнительные вызовы playSound игнорируются, пока игрок не освободится. Вы можете обойти это, «украдя» плеер, который уже используется для воспроизведения нового звука.
  • Между вызовом playSound и воспроизведением звука может быть значительный разрыв. Несмотря на то, что MediaPlayer используется повторно, на самом деле это тонкая оболочка, которая управляет базовым собственным объектом C ++ через JNI. Собственный проигрыватель уничтожается каждый раз, когда вы вызываете MediaPlayer.reset (), и он должен быть воссоздан всякий раз, когда MediaPlayer подготовлен.

Улучшить время ожидания, сохраняя возможность повторного использования игроков, сложнее. К счастью, для определенных типов звуков и приложений, где требуется низкая задержка, есть еще один вариант, который мы рассмотрим в следующий раз: SoundPool.