Deep Dive: MediaPlayer Best Practices

Foto av Marcela Laskoski på Unsplash

MediaPlayer ser ut til å være villedende enkel å bruke, men kompleksiteten lever rett under overflaten. For eksempel kan det være fristende å skrive noe slikt:

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

Dette fungerer fint den første og sannsynligvis den andre, tredje eller enda flere ganger. Imidlertid bruker hver nye MediaPlayer systemressurser, for eksempel minne og kodeker. Dette kan forringe ytelsen til appen din, og muligens hele enheten.

Heldigvis er det mulig å bruke MediaPlayer på en måte som er både enkel og trygg ved å følge noen enkle regler.

Den enkle saken

Det mest grunnleggende tilfellet er at vi har en lydfil, kanskje en rå ressurs, som vi bare vil spille. I dette tilfellet lager vi en enkelt spiller gjenbruk den hver gang vi trenger å spille en lyd. Spilleren skal skapes med noe slikt:

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

Spilleren er opprettet med to lyttere:

  • OnPreparedListener, som automatisk starter avspillingen etter at spilleren er forberedt.
  • OnCompletionListener som automatisk renser opp ressursene når avspillingen er ferdig.

Når spilleren er opprettet, er neste trinn å lage en funksjon som tar en ressurs-ID og bruker denne MediaPlayer til å spille den:

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

Det skjer ganske mye i denne korte metoden:

  • Ressurs-IDen må konverteres til en AssetFileDescriptor fordi dette er det MediaPlayer bruker for å spille råressurser. Nullkontrollen sikrer at ressursen eksisterer.
  • Tilbakestilling av samtalen () sikrer at spilleren er i initialisert tilstand. Dette fungerer uansett hvilken tilstand spilleren er i.
  • Angi datakilden for spilleren.
  • PreparAsync forbereder spilleren til å spille og kommer tilbake umiddelbart, og holder brukergrensesnittet responsivt. Dette fungerer fordi vedlagte OnPreparedListener begynner å spille etter at kilden er klargjort.

Det er viktig å merke oss at vi ikke kaller utgivelse () på spilleren vår eller at den er null. Vi vil gjenbruke det! Så i stedet kaller vi tilbakestilling (), som frigjør minne og kodeker det brukte.

Å spille en lyd er så enkelt som å ringe:

playSound (R.raw.cowbell)

Enkel!

Flere Cowbells

Det er enkelt å spille en lyd om gangen, men hva om du vil starte en annen lyd mens den første fortsatt spiller? Å ringe playSound () flere ganger som dette fungerer ikke:

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

I dette tilfellet begynner R.raw.big_cowbell å bli forberedt, men den andre samtalen tilbakestiller spilleren før noe kan skje, så bare du bare hører R.raw.small_cowbell.

Og hva om vi ville spille flere lyder sammen samtidig? Vi må lage en MediaPlayer for hver enkelt. Den enkleste måten å gjøre dette på er å ha en liste over aktive spillere. Kanskje noe som dette:

klasse MediaPlayers (kontekst: Context) {
    private val context: Context = context.applicationContext
    private val PlayersInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer (). gjelder {
        setOnPreparedListener {start ()}
        setOnCompletionListener {
            it.release ()
            PlayersInUse - = det
        }
    }

    overstyre morsomme playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: return
        val mediaPlayer = buildPlayer ()

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

Nå som hver lyd har sin egen spiller er det mulig å spille både R.raw.big_cowbell og R.raw.small_cowbell sammen! Perfekt!

… Vel, nesten perfekt. Det er ikke noe i koden vår som begrenser antall lyder som kan spilles på en gang, og MediaPlayer trenger fremdeles å ha minne og kodeker å jobbe med. Når de går tom, mislykkes MediaPlayer lydløst, og merker bare “E / MediaPlayer: Error (1, -19)” i logcat.

Gå inn i MediaPlayerPool

Vi ønsker å støtte avspilling av flere lyder på en gang, men vi vil ikke gå tom for minne eller kodeker. Den beste måten å håndtere disse tingene er å ha et basseng med spillere og deretter velge en som skal brukes når vi vil spille en lyd. Vi kan oppdatere koden vår slik at den blir slik:

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

    private val mediaPlayerPool = mutableListOf  (). også {
        for (i i 0..maxStreams) it + = buildPlayer ()
    }
    private val PlayersInUse = mutableListOf  ()

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

    / **
     * Returnerer en [MediaPlayer] hvis en er tilgjengelig,
     * ellers null.
     * /
    privat morsomme forespørselPlayer (): MediaPlayer? {
        return if (! mediaPlayerPool.isEpty ()) {
            mediaPlayerPool.removeAt (0) .så {
                PlayersInUse + = det
            }
        } annet null
    }

    privat morsom recyclePlayer (mediaPlayer: MediaPlayer) {
        mediaPlayer.reset ()
        PlayersInUse - = mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }

    morsom 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 ()
        }
    }
}

Nå kan flere lyder spille av samtidig, og vi kan kontrollere det maksimale antallet samtidige spillere for å unngå å bruke for mye minne eller for mange kodeker. Og siden vi resirkulerer forekomstene, trenger ikke søppelsamleren å løpe for å rydde opp i alle de gamle forekomstene som er ferdige med å spille.

Det er noen få ulemper med denne tilnærmingen:

  • Etter at maxStreams-lydene er spilt, ignoreres eventuelle ekstra samtaler til playSound til en spiller er frigjort. Du kan omgå dette ved å "stjele" en spiller som allerede er i bruk for å spille en ny lyd.
  • Det kan være betydelig etterslep mellom å ringe playSound og faktisk å spille av lyden. Selv om MediaPlayer blir gjenbrukt, er det faktisk en tynn innpakning som kontrollerer et underliggende C ++ native objekt via JNI. Den innfødte spilleren blir ødelagt hver gang du ringer MediaPlayer.reset (), og den må gjenskapes når MediaPlayer er klargjort.

Det er vanskeligere å forbedre latens mens du opprettholder muligheten til å gjenbruke spillere. Heldigvis, for visse typer lyder og apper der det kreves lav latens, er det et annet alternativ vi vil se nærmere på neste gang: SoundPool.