Deep Dive: MediaPlayer Best Practices

Foto door Marcela Laskoski op Unsplash

MediaPlayer lijkt bedrieglijk eenvoudig te gebruiken, maar complexiteit leeft net onder de oppervlakte. Het kan bijvoorbeeld verleidelijk zijn om zoiets te schrijven:

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

Dit werkt prima de eerste en waarschijnlijk de tweede, derde of zelfs meer keren. Elke nieuwe MediaPlayer verbruikt echter systeembronnen, zoals geheugen en codecs. Dit kan de prestaties van uw app en mogelijk van het hele apparaat verslechteren.

Gelukkig is het mogelijk om MediaPlayer op een eenvoudige en veilige manier te gebruiken door een paar eenvoudige regels te volgen.

De eenvoudige zaak

Het meest basale geval is dat we een geluidsbestand hebben, misschien een onbewerkte bron, die we gewoon willen spelen. In dit geval maken we een enkele speler die deze elke keer opnieuw gebruikt als we een geluid moeten afspelen. De speler moet met zoiets worden gemaakt:

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

De speler is gemaakt met twee luisteraars:

  • OnPreparedListener, die het afspelen automatisch start nadat de speler is voorbereid.
  • OnCompletionListener die bronnen automatisch opruimt wanneer het afspelen is voltooid.

Nadat de speler is gemaakt, is de volgende stap het maken van een functie die een resource-ID gebruikt en die MediaPlayer gebruikt om deze af te spelen:

fun playSound (@RawRes rawResId: Int) vervangen
    val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: return
    mediaPlayer.run {
        reset ()
        setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength)
        prepareAsync ()
    }
}

Er gebeurt nogal wat in deze korte methode:

  • De resource-ID moet worden geconverteerd naar een AssetFileDescriptor omdat MediaPlayer dit gebruikt om onbewerkte bronnen af ​​te spelen. De nulcontrole zorgt ervoor dat de bron bestaat.
  • Door reset () aan te roepen, weet je zeker dat de speler de status Geïnitialiseerd heeft. Dit werkt ongeacht in welke staat de speler zich bevindt.
  • Stel de gegevensbron voor de speler in.
  • prepareAsync bereidt de speler voor op het spel en keert onmiddellijk terug, waarbij de UI responsief blijft. Dit werkt omdat bijgevoegde OnPreparedListener begint te spelen nadat de bron is voorbereid.

Het is belangrijk op te merken dat we release () niet op onze speler aanroepen of op nul zetten. We willen het hergebruiken! Dus in plaats daarvan roepen we reset () aan, waardoor het geheugen en de codecs die het gebruikte vrij werden gemaakt.

Een geluid afspelen is net zo eenvoudig als bellen:

PlaySound (R.raw.cowbell)

Eenvoudig!

Meer koebellen

Eén geluid tegelijk afspelen is eenvoudig, maar wat als u een ander geluid wilt starten terwijl het eerste nog speelt? PlaySound () meerdere keren aanroepen zoals dit zal niet werken:

PlaySound (R.raw.big_cowbell)
PlaySound (R.raw.small_cowbell)

In dit geval begint R.raw.big_cowbell zich voor te bereiden, maar het tweede gesprek stelt de speler opnieuw in voordat er iets kan gebeuren, dus u hoort alleen R.raw.small_cowbell.

En wat als we meerdere geluiden tegelijkertijd wilden spelen? We moeten voor elk een MediaPlayer maken. De eenvoudigste manier om dit te doen is om een ​​lijst met actieve spelers te hebben. Misschien zoiets als dit:

class MediaPlayers (context: Context) {
    private val context: Context = context.applicationContext
    private val playersInUse = mutableListOf  ()

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

    fun playSound (@RawRes rawResId: Int) vervangen
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: return
        val mediaPlayer = buildPlayer ()

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

Nu elk geluid zijn eigen speler heeft, is het mogelijk om zowel R.raw.big_cowbell als R.raw.small_cowbell samen te spelen! Perfect!

... Nou ja, bijna perfect. Onze code bevat niets dat het aantal geluiden beperkt dat tegelijkertijd kan worden afgespeeld, en MediaPlayer moet nog geheugen en codecs hebben om mee te werken. Wanneer ze opraken, faalt MediaPlayer stilletjes en merkt alleen "E / MediaPlayer: Fout (1, -19)" in logcat.

Voer MediaPlayerPool in

We willen het spelen van meerdere geluiden tegelijkertijd ondersteunen, maar we willen geen geheugen of codecs hebben. De beste manier om deze dingen te beheren, is om een ​​pool van spelers te hebben en er vervolgens een te kiezen om te gebruiken wanneer we een geluid willen spelen. We kunnen onze code als volgt bijwerken:

class MediaPlayerPool (context: Context, maxStreams: Int) {
    private val context: Context = context.applicationContext

    private val mediaPlayerPool = mutableListOf  (). ook {
        voor (i in 0..maxStreams) it + = buildPlayer ()
    }
    private val playersInUse = mutableListOf  ()

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

    / **
     * Retourneert een [MediaPlayer] als er een beschikbaar is,
     * anders null.
     * /
    private fun requestPlayer (): MediaPlayer? {
        retourneer if (! mediaPlayerPool.isEmpty ()) {
            mediaPlayerPool.removeAt (0) .ook {
                playersInUse + = it
            }
        } anders null
    }

    private fun recyclePlayer (mediaPlayer: MediaPlayer) {
        mediaPlayer.reset ()
        playersInUse - = mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }

    leuk playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: return
        val mediaPlayer = requestPlayer ()?: retour

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

Nu kunnen meerdere geluiden tegelijkertijd worden afgespeeld en kunnen we het maximale aantal gelijktijdige spelers besturen om te voorkomen dat te veel geheugen of te veel codecs worden gebruikt. En omdat we de exemplaren recyclen, hoeft de vuilnisman niet te rennen om alle oude exemplaren die zijn afgespeeld op te ruimen.

Er zijn een paar nadelen aan deze aanpak:

  • Nadat maxStreams-geluiden worden afgespeeld, worden eventuele extra aanroepen van playSound genegeerd totdat een speler wordt vrijgelaten. Je kunt dit omzeilen door een speler die al in gebruik is te 'stelen' om een ​​nieuw geluid te spelen.
  • Er kan een aanzienlijke vertraging zijn tussen het aanroepen van playSound en het daadwerkelijk spelen van het geluid. Hoewel de MediaPlayer opnieuw wordt gebruikt, is het eigenlijk een dunne verpakking die een onderliggend C ++ native object bestuurt via JNI. De native speler wordt vernietigd telkens wanneer u MediaPlayer.reset () aanroept en moet opnieuw worden gemaakt wanneer MediaPlayer wordt voorbereid.

Het is moeilijker om de latentie te verbeteren met behoud van de mogelijkheid om spelers opnieuw te gebruiken. Gelukkig is er voor bepaalde soorten geluiden en apps waar een lage latentie vereist is, een andere optie die we de volgende keer zullen onderzoeken: SoundPool.