Mély merülés: A MediaPlayer bevált gyakorlata

Fotó: Marcela Laskoski az Unsplash-en

Úgy tűnik, hogy a MediaPlayer megtévesztő módon egyszerű, de a bonyolultság közvetlenül a felület alatt él. Például kísértés lehet, hogy így ír:

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

Ez jól működik az első, és valószínűleg a második, harmadik, vagy még ennél is többször. Mindegyik új MediaPlayer rendszer erőforrásokat, például memóriát és kodekeket fogyaszt. Ez ronthatja az alkalmazás, és valószínűleg az egész eszköz teljesítményét.

Szerencsére néhány egyszerű szabályt követve egyszerre és biztonságosan is használható a MediaPlayer.

Az egyszerű eset

A legalapvetőbb eset az, hogy van egy hangfájl, esetleg egy nyers erőforrás, amelyet csak lejátszani akarunk. Ebben az esetben létrehozunk egy lejátszót, aki minden alkalommal újra felhasználja a hang lejátszását. A lejátszót valami hasonlóval kell létrehozni:

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

A lejátszó két hallgatóval jön létre:

  • OnPreparedListener, amely automatikusan elindítja a lejátszást, miután a lejátszó felkészült.
  • OnCompletionListener, amely automatikusan megtisztítja az erőforrásokat, ha a lejátszás befejeződött.

A lejátszó létrehozásával a következő lépés egy olyan funkció létrehozása, amely erőforrás-azonosítót vesz fel, és a MediaPlayer használatával játssza le:

felülírja a szórakoztató playSound-ot (@RawRes rawResId: Int) {
    val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: visszatérés
    mediaPlayer.run {
        Visszaállítás()
        setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength)
        prepareAsync ()
    }
}

Nagyon sok történik ebben a rövid módszerben:

  • Az erőforrás-azonosítót átalakítani kell AssetFileDescriptor-ba, mert ezt használja a MediaPlayer a nyers erőforrások lejátszásához. A null check biztosítja az erőforrás meglétét.
  • A reset () hívása biztosítja, hogy a lejátszó inicializált állapotban legyen. Ez függetlenül attól, hogy milyen állapotban van a játékos.
  • Állítsa be a lejátszó adatforrását.
  • prepaAsync felkészíti a lejátszót a játékra, és azonnal visszatér, miközben az UI reagál. Ez azért működik, mert a csatolt OnPreparedListener a forrás előkészítése után kezd lejátszani.

Fontos megjegyezni, hogy nem hívjuk fel a lejátszót a lejátszóra, vagy nullára állítjuk. Újra használni akarjuk! Tehát ehelyett a reset () -et hívjuk, amely felszabadítja a használt memóriát és kodekeket.

A hang lejátszása ugyanolyan egyszerű, mint a hívás:

playSound (R.raw.cowbell)

Egyszerű!

Több Cowbell

Egyszerre egyszerre lehet lejátszani egy hangot, de mi van, ha egy másik hangot szeretne elindítani, amíg az első még lejátszódik? A PlaySound () többszörös hívása így nem fog működni:

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

Ebben az esetben az R.raw.big_cowbell felkészülni kezd, de a második hívás visszaállítja a lejátszót, mielőtt bármi megtörténhet, tehát csak az R.raw.small_cowbell hallható.

És mi van, ha egyszerre több hangot akarunk játszani? Mindegyikhez létre kell hoznunk egy MediaPlayer-t. Ennek legegyszerűbb módja az aktív játékosok listája. Talán valami ilyesmit:

osztály MediaPlayers (kontextus: Kontextus) {
    privát val kontextus: Context = context.applicationContext
    private val playersInUse = mvableListOf  ()

    private fun buildPlayer () = MediaPlayer ().
        setOnPreparedListener {start ()}
        setOnCompletionListener {
            it.release ()
            playersInUse - = meg
        }
    }

    felülírja a szórakoztató playSound-ot (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: visszatérés
        val mediaPlayer = buildPlayer ()

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

Most, hogy minden hangnak megvan a saját lejátszója, lehetséges az R.raw.big_cowbell és az R.raw.small_cowbell együttes lejátszása! Tökéletes!

… Nos, szinte tökéletes. Kódunkban nincs olyan, amely korlátozza az egyszerre lejátszható hangok számát, és a MediaPlayernek továbbra is rendelkeznie kell memóriával és kodekekkel a működéshez. Amikor elfogynak, a MediaPlayer csendesen kudarcot vall, csak a logcatben megjegyzi az „E / MediaPlayer: Error (1, -19)” -t.

Lépjen be a MediaPlayerPool oldalba

Támogatni akarjuk a több hang egyszerre történő lejátszását, de nem akarjuk kifogyni a memóriából vagy a kodekekből. A legjobb módja ezeknek a dolgoknak a kezelésére, ha van egy játékoscsoport, majd válasszon egyet, amelyet használni szeretne, ha hangot akarunk játszani. Frissíthetjük a kódot, hogy így legyen:

osztály MediaPlayerPool (kontextus: Kontextus, maxStreams: Int) {
    privát val kontextus: Context = context.applicationContext

    private val mediaPlayerPool = mvableListOf  () .also {
        for (i 0..maxStreams-ben) ez + = buildPlayer ()
    }
    private val playersInUse = mvableListOf  ()

    private fun buildPlayer () = MediaPlayer ().
        setOnPreparedListener {start ()}
        setOnCompletionListener {recyclePlayer (it)}
    }

    / **
     * Visszaad egy [MediaPlayer] -t, ha van ilyen,
     * egyébként semmis.
     * /
    privát szórakoztató requestPlayer (): MediaPlayer? {
        visszatérés, ha (! mediaPlayerPool.isEmpty ()) {
            mediaPlayerPool.removeAt (0) .is {
                playersInUse + = meg
            }
        } else null
    }

    privát szórakoztató újrafeldolgozó (mediaPlayer: MediaPlayer) {
        mediaPlayer.reset ()
        playersInUse - = mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }

    szórakoztató playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: visszatérés
        val mediaPlayer = requestPlayer ()?: visszatérés

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

Most több hang is lejátszható egyszerre, és ellenőrizhetjük az egyidejű lejátszók maximális számát, hogy ne kerüljünk túl sok memóriát vagy túl sok kodeket. És mivel újrahasznosítjuk a példányokat, a szemetesgyűjtőnek nem kell futtatnia, hogy megtisztítsa az összes régi példányt, amelyek befejezték a játékot.

Néhány hátránya van ennek a megközelítésnek:

  • Miután a maxStreams hangok lejátszódtak, a további playSound-hívásokat nem veszik figyelembe, amíg a játékos fel nem szabadul. Megkerülheti, ha „ellopja” egy olyan lejátszót, amelyet már használnak egy új hang lejátszásához.
  • A playSound meghívása és a hang lejátszása között jelentős eltérés lehet. Annak ellenére, hogy a MediaPlayer újrafelhasználásra kerül, valójában egy vékony csomagolóeszköz, amely a mögöttes C ++ natív objektumot a JNI-n keresztül kezeli. A natív lejátszó minden alkalommal megsemmisül, amikor meghívja a MediaPlayer.reset () -ot, és a MediaPlayer elkészítésekor újra létre kell hoznia.

A késés javítása, miközben megtartja a játékosok újrafelhasználásának képességét, nehezebb. Szerencsére bizonyos típusú hangok és alkalmazások esetében, ahol alacsony késleltetés szükséges, van egy másik lehetőség, amelyet legközelebb megvizsgálunk: a SoundPool.