原文地址
https://medium.com/uptech-team/audio-not-playing-in-android-cde9a0fdfafd
开篇介绍
Android虽然现在已经是最受欢迎的移动端操作系统,并且有一个庞大的社区,但是有时候依然有那么一些特殊的问题,即使查阅StackOverflow也不能够有效快速的解决。我曾经就遇到了这些问题中的一个,然后我将在这篇文章中分享我关于这个问题一些踩坑的经验给那些需要这些信息的人。
问题大概是这样的:我需要许多的audio音频文件能立刻被播放,并且用户能在需要的时候打开和关闭,而且这些声音能够循环播放。一眼看过去没有什么复杂的地方,当然,如果真的这么简单那么你就不需要阅读这篇文章了:)
我花了相当长的时间来解决这个问题,并且从中收获了很多。在解决这个问题的过程中,我收集了各种网络上的资源以及个人经验,接下来我就会讲解在Android上播放音频文件你可能会遇到的的坑以及解决方案。
不同的方案
首先我们列出Android播放audio文件的一些方案
- MediaPlayer 这是最简单的并且用的最多的一个类,这个不仅能播音频还能播放视频。在这里不会讲解视频方面的细节,只会讲解有关音频的部分。
- SoundPool 在解决问题过程中发现的一个Android class,可以被用来播放小的音频文件,这个类的功能主要是可以同时控制播放多个小的声音。可以看到,我不断的强调这个“小”,简单的解释一下这个原理,SoundPool 接收一个文件(可能从raw文件夹或者是从本地存储)并解压缩成 PCM,一种数字采样模拟信号。最重要的提示就是,每一个解压缩的文件大小不能超过1Mb,否则他们就不会播放。所以SoundPool一般用来播放比较短的声音,比如游戏音效或者类似的东西。
- AudioTrack 也是一个用来播放音频的,不过这个相比于前一个更低级一点,一般只能用于播放解码后的PCM流或者是不需要解码的wav文件。
- ExoPlayer 这是google推荐用来替代 MediaPlayer的一个播放器,在开始这篇文章的时候,release的版本是1.5.11,然后Google发布了2.0.0版本,对比前者有了一些改变。
详细介绍
首先从 MediaPlayer 开始,这应该是上述列表中用的最多的一种了,提到这个不得不放出一个图,
这个图展示了一个MediaPlayer的生命周期...这是一个大的状态机,你需要试着去理解整个工作流的运作,状态的切换。Google对于怎么使用MediaPlayer有一个比较好的 引导 ,如果你没有使用过这个类,可以先读一下整个文档,这里我就直接讲解存在的问题了。
- 多个MediaPlayer的实例在Nexus 5和Nexus 5x上有可能不能同时播放,我已经测试过这些设备,在Nexus 6p这款上面还不能确定,其中的原因还不清楚。
- 另外一个问题就是MediaPlayer的
isPlaying()
方法在播放audio音频结束后依然有可能返回true,比如下面这段代码,在onCompletion回调中打印出isPlaying的结果
private void init() {
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
// some init goes here...
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Log.d("MediaPlayer", "onComplete, mediaPlayer.isPlaying() returns " + mediaPlayer.isPlaying());
}
});
}
从下面的图可以看出确实在结束后依然打印出了true。
- 除此之外,MediaPlayer中的
setVolume()
方法在Jelly Bean API 16可能不会正常的工作,虽然现在这个问题只是出现在LG Optimus这款设备上。为了解决这个问题不得已采用的Android中的AudioManager来通过STREAM_MUSIC设置音量,就像下面这样来减少音量
private void decreaseVolume() {
AudioManager audioManager = ((AudioManager) getSystemService(Context.AUDIO_SERVICE));
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (currentVolume > 0) {
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume - 1, 0);
}
}
这种做法其实有很多的缺点,通过这种方式来设置音量会改变所有的音频流,也就是说,所有使用MediaPlayer播放的程序以及其他的app都会一起共用这个音量,这明显不是一个好的方式。
- MediaPlayer的OnErrorListener 接口回调方法有一个方法
onError(MediaPlayer mp, int what, int extra)
,虽然官方已经有 文档 提到了一些接收的code码,但是依然有一些超出了文档的范围,比如,你可能得到一个code像这个图一样
(-38,0),这个实际上表现网络存在问题,除此之外,也有一些其他的奇奇怪怪的code,也是文档没有提及的。
SoundPool. 因为这个对于文件大小的限制,我其实一直没有用过它,但是在how-bad-is-android-soundpool-what-alternative-to-use 这个问题上对于这个类的用途有一个比较详细的描述,所以可以直接看这个就好了。当然作为个人建议,如果你的文件超过30秒,那么最好不要选择这个。
**AudioTrack. ** 正如前面所说的那样,这个类有很大的局限性,一般不推荐使用这个,如果一定想要了解这个,可以看一下这篇引导, AudioTrack tutorial
ExoPlayer. 了解如何使用这个可以先check一份官方的资料 the official page ,这个库的可定制性是很强的,几乎可以实现一切你需要的需求。我本来想说一些我用这个时遇到过的一些问题,但是在发布的2.0.0版本中已经全部修复了 : ) ,当然我也会提醒那些还没有迁移到2.0版本的,对比中才能发现进步。在1.5.11版本中循环这个功能还不可靠(可用),你不得以在 onComplete
回调中去手动重启你的播放器,这会导致在下一个播放之前出现一个间隔。但是在2.0+的版本中,已经有了一个LoopingMediaSource ,这个类的效果让你感觉不到一个文件的播放结束或开始,无间隙的回放也已经支持。
在第一个版本中我们在没有其他组件的情况下不能设置音量,不得不采取发送一个message的方式,就像下面这样
private void setVolume(float volume) {
ExoPlayer player = ExoPlayer.Factory.newInstance(1);
SampleSource source = new ExtractorSampleSource(Uri.parse("audiourl"),
new DefaultUriDataSource(this, Util.getUserAgent(this, getString(R.string.app_name))),
new DefaultAllocator(BUFFER_SEGMENT_SIZE), BUFFER_SEGMENT_SIZE * BUFFER_SEGMENT_COUNT);
MediaCodecAudioTrackRenderer renderer = new MediaCodecAudioTrackRenderer(source,
MediaCodecSelector.DEFAULT);
player.prepare(renderer);
player.sendMessage(renderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, volume);
}
而从2.0.0版本开始已经在SimpleExoPlayer中已经有了一个 setVolume
方法,这十分的方便,并且修复了ExoPlayer 在API 16 Jelly Bean上部分机型的问题,问题如这个issue所提,this issue。
根据这么多的研究以及个人使用经验,我只能提一个建议:使用ExoPlayer,主要基于以下几个原因,社区更新很快,有问题及时反馈和解决,定制能力没有其他的library可以与之相比,而且使用起来十分简单,没有任何难点。
相关资源:
How bad is SoundPool? What alternative to use?
Multiple MediaPlayers do not work on Nexus 5
Unable to play two MediaPlayer at same time in Nexus 5
Choppy Audio with ofxAndroidSoundPlayer (MediaPlayer)