Deep Dive: Βέλτιστες πρακτικές του MediaPlayer

Φωτογραφία από την Marcela Laskoski στο Unsplash

Το MediaPlayer φαίνεται να είναι απατηλά απλό στη χρήση, αλλά η πολυπλοκότητα ζει ακριβώς κάτω από την επιφάνεια. Για παράδειγμα, μπορεί να είναι δελεαστικό να γράψουμε κάτι τέτοιο:

MediaPlayer.create (περιβάλλον, R.raw.cowbell) .start ()

Αυτό λειτουργεί καλά την πρώτη και ίσως τη δεύτερη, την τρίτη, ή ακόμα περισσότερες φορές. Ωστόσο, κάθε νέο MediaPlayer καταναλώνει πόρους συστήματος, όπως μνήμη και κωδικοποιητές. Αυτό μπορεί να υποβαθμίσει την απόδοση της εφαρμογής σας και ενδεχομένως ολόκληρης της συσκευής.

Ευτυχώς, είναι δυνατή η χρήση του MediaPlayer με τρόπο τόσο απλό όσο και ασφαλές ακολουθώντας μερικούς απλούς κανόνες.

Η απλή υπόθεση

Η πιο βασική περίπτωση είναι ότι έχουμε ένα αρχείο ήχου, ίσως μια ακατέργαστη πηγή, που θέλουμε απλώς να παίξουμε. Σε αυτή την περίπτωση θα δημιουργήσουμε ένα μόνο παίκτη να το επαναχρησιμοποιούμε κάθε φορά που χρειαζόμαστε να παίξουμε έναν ήχο. Ο παίκτης πρέπει να δημιουργηθεί με κάτι τέτοιο:

ιδιωτικό val mediaPlayer = MediaPlayer () εφαρμόστε {
    setOnPreparedListener {αρχή ()}
    setOnCompletionListener {επαναφορά ()}
}}

Ο παίκτης δημιουργείται με δύο ακροατές:

  • OnPreparedListener, η οποία θα ξεκινήσει αυτόματα την αναπαραγωγή μετά την προετοιμασία της συσκευής αναπαραγωγής.
  • OnCompletionListener που καθαρίζει αυτόματα τους πόρους όταν έχει τελειώσει η αναπαραγωγή.

Με το πρόγραμμα αναπαραγωγής που δημιουργήθηκε, το επόμενο βήμα είναι να δημιουργήσετε μια συνάρτηση που λαμβάνει ένα αναγνωριστικό πόρων και χρησιμοποιεί το MediaPlayer για να το αναπαράγει:

αντικαταστήσει τη διασκέδαση playSound (@RawRes rawResId: Int) {
    val assetFileDescriptor = context.resources.openRawResourceFd (rawResId);: επιστροφή
    mediaPlayer.run {
        επαναφορά()
        setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength)
        prepareAsync ()
    }}
}}

Κάπως συμβαίνει σε αυτή τη σύντομη μέθοδο:

  • Το αναγνωριστικό πόρου πρέπει να μετατραπεί σε ένα AssetFileDescriptor, επειδή αυτό είναι που χρησιμοποιεί το MediaPlayer για την αναπαραγωγή ακατέργαστων πόρων. Ο έλεγχος null εξασφαλίζει ότι ο πόρος υπάρχει.
  • Η επαναφορά κλήσης () εξασφαλίζει ότι η συσκευή αναπαραγωγής βρίσκεται στην αρχική κατάσταση. Αυτό λειτουργεί ανεξάρτητα από την κατάσταση στην οποία βρίσκεται ο παίκτης.
  • Ορίστε την πηγή δεδομένων για τη συσκευή αναπαραγωγής.
  • prepareAsync προετοιμάζει τον παίκτη για να παίξει και να επιστρέψει αμέσως, διατηρώντας την UI απόκριση. Αυτό συμβαίνει επειδή το συνδεδεμένο OnPreparedListener αρχίζει να παίζει μετά την προετοιμασία της πηγής.

Είναι σημαντικό να σημειώσουμε ότι δεν ονομάζουμε απελευθέρωση () στον παίκτη μας ή το θέσαμε σε null. Θέλουμε να το επαναχρησιμοποιήσουμε! Επομένως, καλούμε το reset (), το οποίο ελευθερώνει τη μνήμη και τους κωδικοποιητές που χρησιμοποιεί.

Η αναπαραγωγή ενός ήχου είναι τόσο απλή όσο η κλήση:

playSound (R.raw.cowbell)

Απλός!

Περισσότερα Cowbells

Η αναπαραγωγή ενός ήχου κάθε φορά είναι εύκολη, αλλά τι γίνεται εάν θέλετε να ξεκινήσετε έναν άλλο ήχο ενώ ο πρώτος ακούγεται ακόμα; Η κλήση του playSound () πολλές φορές σαν αυτό δεν θα λειτουργήσει:

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

Σε αυτή την περίπτωση, ο R.raw.big_cowbell αρχίζει να προετοιμάζεται, αλλά η δεύτερη κλήση επαναφέρει τον παίκτη πριν γίνει οτιδήποτε, οπότε ακούτε μόνο το R.raw.small_cowbell.

Και τι εάν θέλαμε να παίξουμε ταυτόχρονα πολλούς ήχους; Θα χρειαζόταν να δημιουργήσουμε ένα MediaPlayer για καθένα από αυτά. Ο απλούστερος τρόπος για να γίνει αυτό είναι να έχετε έναν κατάλογο ενεργών παικτών. Ίσως κάτι τέτοιο:

class MediaPlayers (περιβάλλον: Πλαίσιο) {
    ιδιωτικό πλαίσιο context: Context = context.applicationContext
    ιδιωτικοί παίκτες valInUse = mutableListOf  ()

    ιδιωτική διασκέδαση buildPlayer () = MediaPlayer () εφαρμόστε {
        setOnPreparedListener {αρχή ()}
        setOnCompletionListener {
            it.release ()
            playersInuse - = αυτό
        }}
    }}

    αντικαταστήσει τη διασκέδαση playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId);: επιστροφή
        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: Σφάλμα (1, -19)" στο logcat.

Εισαγάγετε το MediaPlayerPool

Θέλουμε να υποστηρίξουμε την αναπαραγωγή πολλών ήχων ταυτόχρονα, αλλά δεν θέλουμε να εξαντληθεί η μνήμη ή οι κωδικοποιητές. Ο καλύτερος τρόπος για να διαχειριστείτε αυτά τα πράγματα είναι να έχετε μια ομάδα παικτών και στη συνέχεια επιλέξτε ένα για να το χρησιμοποιήσετε όταν θέλουμε να παίξουμε έναν ήχο. Θα μπορούσαμε να ενημερώσουμε τον κώδικα μας ως εξής:

class MediaPlayerPool (πλαίσιο: Πλαίσιο, maxStreams: Int) {
    ιδιωτικό πλαίσιο context: Context = context.applicationContext

    ιδιωτικό val mediaPlayerPool = mutableListOf  () επίσης επίσης {
        για (i σε 0..maxStreams) it + = buildPlayer ()
    }}
    ιδιωτικοί παίκτες valInUse = mutableListOf  ()

    ιδιωτική διασκέδαση buildPlayer () = MediaPlayer () εφαρμόστε {
        setOnPreparedListener {αρχή ()}
        setOnCompletionListener {recyclePlayer (it)}
    }}

    / **
     * Επιστρέφει ένα [MediaPlayer] αν είναι διαθέσιμο,
     * αλλιώς μηδενική.
     * /
    ιδιωτικό requestPlayer (): MediaPlayer; {
        επιστροφή αν (! mediaPlayerPool.isEmpty ()) {
            mediaPlayerPool.removeAt (0) .also {
                playersInuse + = αυτό
            }}
        } else null
    }}

    ιδιωτική διασκέδασηPlayer (MediaPlayer: MediaPlayer) {
        mediaPlayer.reset ()
        playersInUse - = mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }}

    fun playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId);: επιστροφή
        val mediaPlayer = requestPlayer ();: επιστροφή

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

Τώρα πολλοί ήχοι μπορούν να αναπαραχθούν ταυτόχρονα και μπορούμε να ελέγξουμε τον μέγιστο αριθμό ταυτόχρονων παικτών για να αποφύγουμε τη χρήση υπερβολικής μνήμης ή πολλών κωδικοποιητών. Και επειδή ανακυκλώνουμε τις περιπτώσεις, ο συλλέκτης σκουπιδιών δεν θα πρέπει να τρέξει για να καθαρίσει όλες τις παλιές στιγμές που έχουν τελειώσει.

Υπάρχουν μερικά μειονεκτήματα αυτής της προσέγγισης:

  • Μετά την αναπαραγωγή ήχων maxStreams, τυχόν πρόσθετες κλήσεις προς το playSound αγνοούνται μέχρι να απελευθερωθεί ένας παίκτης. Θα μπορούσατε να το αντιμετωπίσετε με "κλοπή" ενός παίκτη που είναι ήδη σε χρήση για να παίξει ένα νέο ήχο.
  • Μπορεί να υπάρξει σημαντική καθυστέρηση μεταξύ της κλήσης του playSound και της πραγματικής αναπαραγωγής του ήχου. Παρόλο που το MediaPlayer επαναχρησιμοποιείται, είναι στην πραγματικότητα ένα λεπτό περιτύλιγμα που ελέγχει ένα υποκείμενο εγγενές αντικείμενο C ++ μέσω JNI. Ο εγγενής παίκτης καταστρέφεται κάθε φορά που καλείτε το MediaPlayer.reset () και πρέπει να αναπαραχθεί κάθε φορά που προετοιμάζεται το MediaPlayer.

Η βελτίωση της λανθάνουσας κατάστασης, διατηρώντας παράλληλα την ικανότητα επαναχρησιμοποίησης των παικτών, γίνεται πιο δύσκολη. Ευτυχώς, για ορισμένους τύπους ήχων και εφαρμογών όπου απαιτείται χαμηλή λανθάνουσα κατάσταση, υπάρχει μια άλλη επιλογή που θα εξετάσουμε την επόμενη φορά: SoundPool.