Deep Dive: MediaPlayer Best Practices
MediaPlayer seems to be deceptively uncomplicated to use, but complexity lives only below the surface. For case, it can be tempting to write something like this:
MediaPlayer.create(context, R.raw.cowbell).start()
This works fine the first and probably the second, third, or even more times. However, each new MediaPlayer consumes system resources, such as memory and, codecs. This tin can dethrone the operation of your app, and possibly the unabridged device.
Fortunately, it's possible to use MediaPlayer in a fashion that is both simple and safe past following a few simple rules.
The Simple Case
The nearly bones case is that we have a sound file, peradventure a raw resource, that we just desire to play. In this case we'll create a unmarried player reuse it each time we need to play a sound. The player should exist created with something like this:
private val mediaPlayer = MediaPlayer().apply {
setOnPreparedListener { start() }
setOnCompletionListener { reset() }
}
The player is created with two listeners:
- OnPreparedListener, which will automatically showtime playback after the actor has been prepared.
- OnCompletionListener which automatically cleans upwardly resources when playback has finished.
With the histrion created, the adjacent pace is to make a function that takes a resources ID and uses that MediaPlayer to play it:
override fun playSound(@RawRes rawResId: Int) {
val assetFileDescriptor = context.resource.openRawResourceFd(rawResId) ?: return
mediaPlayer.run {
reset()
setDataSource(assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength)
prepareAsync()
}
}
In that location's quite a fleck happening in this short method:
- The resource ID must be converted to an AssetFileDescriptor considering this is what MediaPlayer uses to play raw resources. The zip check ensures the resource exists.
- Calling reset() ensures the player is in the Initialized state. This works no thing what state the actor is in.
- Prepare the data source for the player.
- prepareAsync prepares the thespian to play and returns immediately, keeping the UI responsive. This works because fastened OnPreparedListener starts playing after the source has been prepared.
It's of import to note we don't phone call release() on our actor or set it to null. Nosotros want to reuse information technology! Then instead we telephone call reset(), which frees the memory and codecs it was using.
Playing a audio is equally elementary equally calling:
playSound(R.raw.cowbell)
Unproblematic!
More Cowbells
Playing 1 sound at a time is easy, but what if y'all want to get-go another audio while the first one is nonetheless playing? Calling playSound()
multiple times like this won't work:
playSound(R.raw.big_cowbell)
playSound(R.raw.small_cowbell)
In this instance, R.raw.big_cowbell
starts to get prepared, but the second call resets the thespian before anything can happen, and then only yous merely hear R.raw.small_cowbell
.
And what if we wanted to play multiple sounds together at the aforementioned time? We'd need to create a MediaPlayer for each one. The simplest manner to practice this is to have a listing of active players. Perhaps something similar this:
course MediaPlayers(context: Context) {
private val context: Context = context.applicationContext
individual val playersInUse = mutableListOf<MediaPlayer>() private fun buildPlayer() = MediaPlayer().apply {
setOnPreparedListener { outset() }
setOnCompletionListener {
it.release()
playersInUse -= it
}
} override fun
playSound(@RawRes rawResId: Int) {
val assetFileDescriptor = context.resources.openRawResourceFd(rawResId) ?: return
val mediaPlayer = buildPlayer()
mediaPlayer.run {
playersInUse += it
setDataSource(assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset,
assetFileDescriptor.declaredLength)
prepareAsync()
}
}
}
Now that every sound has its own histrion it's possible to play both R.raw.big_cowbell
and R.raw.small_cowbell
together! Perfect!
… Well, almost perfect. There's not anything in our code that limits the number of sounds that can play at one time, and MediaPlayer still needs to have memory and codecs to work with. When they run out, MediaPlayer fails silently, only noting "E/MediaPlayer: Fault (1,-xix)
" in logcat.
Enter MediaPlayerPool
We want to support playing multiple sounds at once, but we don't want to run out of retentivity or codecs. The all-time way to manage these things is to take a pool of players and so pick 1 to utilise when we want to play a sound. Nosotros could update our code to be like this:
course MediaPlayerPool(context: Context, maxStreams: Int) {
private val context: Context = context.applicationContext private val mediaPlayerPool = mutableListOf<MediaPlayer>().also {
for (i in 0..maxStreams) information technology += buildPlayer()
}
private val playersInUse = mutableListOf<MediaPlayer>() private fun buildPlayer() = MediaPlayer().apply {
setOnPreparedListener { start() }
setOnCompletionListener { recyclePlayer(it) }
} /**
* Returns a [MediaPlayer] if one is available,
* otherwise zilch.
*/
private fun requestPlayer(): MediaPlayer? {
render if (!mediaPlayerPool.isEmpty()) {
mediaPlayerPool.removeAt(0).also {
playersInUse += it
}
} else cipher
}
private fun recyclePlayer(mediaPlayer: MediaPlayer) {
mediaPlayer.reset()
playersInUse -= mediaPlayer
mediaPlayerPool += mediaPlayer
}
fun playSound(@RawRes rawResId: Int) {
val assetFileDescriptor = context.resources.openRawResourceFd(rawResId) ?: return
val mediaPlayer = requestPlayer() ?: returnmediaPlayer.run {
setDataSource(assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset,
assetFileDescriptor.declaredLength)
prepareAsync()
}
}
}
Now multiple sounds can play at once, and we can control the maximum number of simultaneous players to avert using as well much memory or too many codecs. And, since we're recycling the instances, the garbage collector won't accept to run to make clean up all of the old instances that have finished playing.
There are a few downsides to this approach:
- After maxStreams sounds are playing, whatsoever additional calls to playSound are ignored until a player is freed upwards. Y'all could work around this past "stealing" a player that's already in employ to play a new audio.
- In that location tin be significant lag between calling playSound and actually playing the sound. Even though the MediaPlayer is being reused, information technology's actually a thin wrapper that controls an underlying C++ native object via JNI. The native player is destroyed each fourth dimension you call
MediaPlayer.reset()
, and information technology must be recreated whenever the MediaPlayer is prepared.
Improving latency while maintaining the ability to reuse players is harder to exercise. Fortunately, for certain types of sounds and apps where low latency is required, there's another option that we'll await into next time: SoundPool.
DOWNLOAD HERE
Posted by: montaltoequitiardead.blogspot.com
Post a Comment