Deep Dive: MediaPlayer bedste praksis

Foto af Marcela Laskoski på Unsplash

MediaPlayer ser ud til at være vildledende enkel at bruge, men kompleksiteten lever lige under overfladen. For eksempel kan det være fristende at skrive noget som dette:

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

Dette fungerer fint den første og sandsynligvis den anden, tredje eller endnu flere gange. Hver nye MediaPlayer bruger imidlertid systemressourcer, såsom hukommelse og codecs. Dette kan forringe ydeevnen for din app og muligvis hele enheden.

Heldigvis er det muligt at bruge MediaPlayer på en måde, der er både enkel og sikker ved at følge et par enkle regler.

Den enkle sag

Det mest basale tilfælde er, at vi har en lydfil, måske en rå ressource, som vi bare vil spille. I dette tilfælde opretter vi en enkelt afspiller, der bruger den igen, hver gang vi har brug for at afspille en lyd. Spilleren skal oprettes med noget lignende:

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

Afspilleren er oprettet med to lyttere:

  • OnPreparedListener, som automatisk starter afspilningen, når afspilleren er forberedt.
  • OnCompletionListener, der automatisk renser ressourcer, når afspilningen er afsluttet.

Når afspilleren er oprettet, er det næste trin at oprette en funktion, der tager et ressource-ID og bruger denne MediaPlayer til at afspille den:

tilsidesætte sjovt playSound (@RawRes rawResId: Int) {
    val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: return
    mediaPlayer.run {
        Nulstil()
        setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength)
        prepareAsync ()
    }
}

Der sker ganske lidt i denne korte metode:

  • Ressource-ID'et skal konverteres til en AssetFileDescriptor, fordi det er dette, MediaPlayer bruger til at afspille rå ressourcer. Nullkontrollen sikrer, at ressourcen findes.
  • Opkaldsnulstilling () sikrer, at afspilleren er i initialiseret tilstand. Dette fungerer uanset hvilken tilstand spilleren er i.
  • Indstil datakilden for afspilleren.
  • PreparAsync forbereder afspilleren til at spille og vender straks tilbage, hvilket holder brugergrænsefladen brugervenlig. Dette fungerer, fordi vedhæftede OnPreparedListener begynder at spille, når kilden er blevet forberedt.

Det er vigtigt at bemærke, at vi ikke kalder frigivelse () på vores afspiller eller indstiller den til null. Vi vil genbruge det! Så i stedet kalder vi reset (), som frigør hukommelsen og codecs, den brugte.

Afspilning af en lyd er så simpelt som at ringe:

playSound (R.raw.cowbell)

Enkel!

Flere cowbells

Det er let at spille en lyd ad gangen, men hvad nu hvis du vil starte en anden lyd, mens den første stadig spiller? At ringe til playSound () flere gange som dette fungerer ikke:

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

I dette tilfælde begynder R.raw.big_cowbell at blive forberedt, men det andet opkald nulstiller afspilleren, før noget kan ske, så kun du kun hører R.raw.small_cowbell.

Og hvad hvis vi ville spille flere lyde sammen på samme tid? Vi bliver nødt til at oprette en MediaPlayer til hver enkelt. Den enkleste måde at gøre dette på er at have en liste over aktive spillere. Måske noget lignende:

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

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

    tilsidesætte sjovt 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 ()
        }
    }
}

Nu hvor hver lyd har sin egen afspiller er det muligt at spille både R.raw.big_cowbell og R.raw.small_cowbell sammen! Perfekt!

… Godt, næsten perfekt. Der er ikke noget i vores kode, der begrænser antallet af lyde, der kan afspilles på én gang, og MediaPlayer skal stadig have hukommelse og codecs til at arbejde med. Når de løber tør, mislykkes MediaPlayer lydløst og bemærker kun “E / MediaPlayer: Error (1, -19)” i logcat.

Gå ind i MediaPlayerPool

Vi ønsker at støtte afspilning af flere lyde på én gang, men vi vil ikke løbe tør for hukommelse eller codecs. Den bedste måde at styre disse ting på er at have en pool af spillere og derefter vælge en, der skal bruges, når vi vil spille en lyd. Vi kunne opdatere vores kode til at være sådan:

klasse MediaPlayerPool (kontekst: Context, maxStreams: Int) {
    privat valkontekst: Context = context.applicationContext

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

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

    / **
     * Returnerer en [MediaPlayer], hvis en er tilgængelig,
     * ellers nul.
     * /
    privat sjov forespørgselPlayer (): MediaPlayer? {
        vende tilbage hvis (! mediaPlayerPool.isEpty ()) {
            mediaPlayerPool.removeAt (0). også {
                PlayersInUse + = det
            }
        } andet null
    }

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

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

Nu kan flere lyde afspilles på én gang, og vi kan kontrollere det maksimale antal samtidige spillere for at undgå at bruge for meget hukommelse eller for mange codecs. Og da vi genanvender forekomsterne, behøver affaldsopsamleren ikke at løbe for at rydde op i alle de gamle tilfælde, der er færdige med at spille.

Der er nogle få ulemper ved denne tilgang:

  • Efter at maxStreams-lyde spiller, ignoreres eventuelle yderligere opkald til playSound, indtil en spiller er frigivet. Du kan omgå dette ved at "stjæle" en afspiller, der allerede er i brug til at afspille en ny lyd.
  • Der kan være en betydelig forsinkelse mellem at kalde playSound og faktisk at afspille lyden. Selvom MediaPlayer genbruges, er det faktisk en tynd indpakning, der kontrollerer et underliggende C ++ native objekt via JNI. Den indbyggede afspiller ødelægges, hver gang du ringer til MediaPlayer.reset (), og den skal gendannes, når MediaPlayer er klar.

Det er sværere at forbedre latens og samtidig bevare muligheden for at genbruge spillere. For visse typer lyde og apps, hvor lav latens er påkrævet, er der heldigvis en anden mulighed, som vi vil undersøge næste gang: SoundPool.