在android 中随着手机性能的不断完善和提高,传统的图文慢慢转向了音视频,因为相对于音视频的信息传递,传统的图文就显得有些慢了,所以现在很多的APP里面或多或少都会包含一些音视频的功能在里面,所以对于开发者而言,学习和掌握音视频技术就显得比较重要了。废话不多说,直接进入正题。
音频部分分为两大块,一个是系统自带的MediaPlayer,一个是框架ExoPlayer。
系统自带的MediaPlayer
看过源码的同学都知道,MediaPlayer在应用层并没有做太多的事情,大部分的工作全部是交给Native层去实现的,也就是交给C/C++去实现的。而对于应用层来说,使用起来非常简单,直接调用系统的API就可以了,现在具体来看看。
第一步:创建MediaPlayer
创建MediaPlayer的方式有两种:
通过new的方式去获取MediaPlayer的实例
MediaPlayer mp = new MediaPlayer();
使用create的方式
MediaPlayer mp = MediaPlayer.create(this, R.raw.test);//这时就不用调用setDataSource
第二步:设置播放的音频文件
MediaPlayer要播放的文件主要包括3个来源:
a. 用户在应用中事先自带的resource资源
例如:MediaPlayer.create(this, R.raw.test);
b. 存储在SD卡或其他文件路径下的媒体文件
例如:mp.setDataSource("/sdcard/test.mp3");
c. 网络上的媒体文件
例如:mp.setDataSource("http://www.citynorth.cn/music/confucius.mp3");
在这里需要强调的是MediaPlayer的setDataSource的重载方法有很多,下面重点介绍以下几种:
(1)setDataSource (String path)
其中path可以是本地的音频文件的绝对路径也可以是网络路径
(2)setDataSource (Context context, Uri uri)
将本地的资源转换为Uri
(3)setDataSource (FileDescriptor fd, long offset, long length)
使用FileDescriptor时,需要将文件放到与res文件夹平级的assets文件夹里。
AssetFileDescriptor fileDescriptor = getAssets().openFd("rain.mp3");
mediaPlayer.setDataSource(fileDescriptor.getFileDescriptor(),fileDescriptor.getStartOffset(), fileDescriptor.getLength());
第三步:装载媒体资源
在设置好播放的媒体资源之后还需要做一个操作就是装载资源,也就是将流设置到MediaPlayer当中去,系统提供了两种装载资源的方法,即使用prepare()或prepareAsync()方法把流媒体装载进MediaPlayer,才可以调用start()方法播放流媒体。一般情况下我们为了防止堵塞主线程会采取异步装载的方式,即
mediaPlayer.prepareAsync()
第四步:执行播放操作
在装载结束后,可以直接调用start()
方法来开始播放我们的音频文件了,那么这里就会有一个疑问,什么时候知道装载结束了呢?不要慌,系统为我们提供了监听的方法setOnPreparedListener
mediaPlayer?.setOnPreparedListener {
mediaPlayer?.start()
}
至此,一个完整的播放流程就结束了,当然除此之外还有一些比较重要的知识点,如下所示。
void start():开始或者恢复播放
void stop():停止播放
void pause():暂停播放
这3个方法是控制播放状态的,除此之外还有一些其他的API,如下所示
int getDuration():获取流媒体的总播放时长,单位是毫秒。
int getCurrentPosition():获取当前流媒体的播放的位置,单位是毫秒。
void seekTo(int msec):设置当前MediaPlayer的播放位置,单位是毫秒。
void setLooping(boolean looping):设置是否循环播放。
boolean isLooping():判断是否循环播放。
boolean isPlaying():判断是否正在播放。
void prepare():同步的方式装载流媒体文件。
void prepareAsync():异步的方式装载流媒体文件。
void release ():回收流媒体资源。
void setAudioStreamType(int streamtype):设置播放流媒体类型。
void setWakeMode(Context context, int mode):设置CPU唤醒的状态。
需要强调说明的是:
setAudioStreamType()方法用于指定播放流媒体的类型,它传递的是一个int类型的数据,均以常量定义在AudioManager类中, 一般我们播放音频文件,设置为AudioManager.STREAM_MUSIC即可
知识点二:MediaPlayer的回调
(1) setOnCompletionListener(MediaPlayer.OnCompletionListener listener):当流媒体播放完毕的时候回调。
(2) setOnErrorListener(MediaPlayer.OnErrorListener listener):当播放中发生错误的时候回调。
(3) setOnPreparedListener(MediaPlayer.OnPreparedListener listener):当装载流媒体完毕的时候回调。
(4) setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener listener):当使用seekTo()设置播放位置的时候回调。
知识点三:MediaPlayer的回收
使用完MediaPlayer需要回收资源。MediaPlayer是很消耗系统资源的,所以在使用完MediaPlayer,不要等待系统自动回收,最好是主动回收资源。
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
其实在实际开发中,我们应该将播放的逻辑与Service相结合,这里为了测试效果就没有写的那么完整
class MediaPlayerActivity : AppCompatActivity() {
private var mediaPlayer: MediaPlayer? = null
private var mList: MutableList<String> = mutableListOf()
private var mCurrentPlayPosition = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initMusic()
initMediaPlayer()
}
/**
* 加载音频文件
*/
private fun initMusic() {
mList.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/2e76a133b898a2f6c8fb62e963b87ce4_tongkuercanlandeyisheng.mp3")
mList.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/e845a564e795c628c6bdd652ddbc24e3_fulidadeqingshaonianshidai.mp3")
mList.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/e9b9abdcc3855586ec2c4651293c11e9_abuyuwulei.mp3")
mList.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/ba2eaf007614fdf81c8d9e895ba88ff2_abuchuangzuodekaishi.mp3")
}
/**
* 初始化MediaPlayer
*/
private fun initMediaPlayer() {
// 1.实例化MediaPlayer
// 也可以使用create的方式,如:
// MediaPlayer mp = MediaPlayer.create(this, R.raw.test);//这时就不用调用setDataSource了
mediaPlayer = MediaPlayer()
// 2.设置播放的音频文件
// 设置的音频文件的来源有3种
// a. 用户在应用中事先自带的resource资源
// 例如:MediaPlayer.create(this, R.raw.test);
// b. 存储在SD卡或其他文件路径下的媒体文件,传入的是本地音频文件的绝对路径
// 例如:mp.setDataSource("/sdcard/test.mp3");
// c. 网络上的媒体文件,传入的是网络地址
// 例如:mp.setDataSource("http://www.citynorth.cn/music/confucius.mp3");
mediaPlayer?.setDataSource(mList[mCurrentPlayPosition])
// MediaPlayer的setDataSource一共四个方法:
// setDataSource (String path)
// setDataSource (FileDescriptor fd)
// setDataSource (Context context, Uri uri)
// setDataSource (FileDescriptor fd, long offset, long length)
// 其中使用FileDescriptor时,需要将文件放到与res文件夹平级的assets文件夹里,然后使用:
// AssetFileDescriptor fileDescriptor = getAssets().openFd("rain.mp3");
// mediaPlayer.setDataSource(fileDescriptor.getFileDescriptor(),fileDescriptor.getStartOffset(), fileDescriptor.getLength());来设置datasource
// 3.装载我们的音频文件
mediaPlayer?.prepareAsync()
// 4.播放我们的音频文件
mediaPlayer?.setOnPreparedListener {
mediaPlayer?.start()
}
}
/**
* 上一首
*/
fun lastMusic(view: View) {
mediaPlayer?.reset()
if (mCurrentPlayPosition == 0) {
mCurrentPlayPosition = mList.size - 1
} else {
mCurrentPlayPosition--
}
mediaPlayer?.setDataSource(mList[mCurrentPlayPosition])
mediaPlayer?.prepareAsync()
mediaPlayer?.setOnPreparedListener {
mediaPlayer?.start()
}
}
/**
* 播放
*/
fun startMusic(view: View) {
mediaPlayer?.start()
}
/**
* 暂停
*/
fun pauseMusic(view: View) {
mediaPlayer?.pause()
}
/**
* 结束
*/
fun stopMusic(view: View) {
mediaPlayer?.stop()
}
/**
* 下一首
*/
fun nextMusic(view: View) {
try {
mediaPlayer?.reset()
if (mCurrentPlayPosition == mList.size - 1) {
mCurrentPlayPosition = 0
} else {
mCurrentPlayPosition++
}
mediaPlayer?.setDataSource(mList[mCurrentPlayPosition])
mediaPlayer?.prepareAsync()
mediaPlayer?.setOnPreparedListener {
mediaPlayer?.start()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
框架ExoPlayer
ExoPlayer是运行在YouTube app Android版本上的视频播放器而并非系统内置的API,所以相对于MediaPlayer来说,其功能更加强大,除了MediaPlayer本身所支持的格式外,ExoPlayer还可以支持DASH和SmoothStreaming格式。接下来我们就一起来看看吧。
ExoPlayer库的核心是ExoPlayer接口。ExoPlayer公开了传统的高水平媒体播放器的功能,例如媒体缓冲,播放,暂停和快进功能。ExoPlayer实现旨在对正在播放的媒体类型,存储方式和位置以及渲染方式做出一些假设(因此几乎没有限制)。ExoPlayer没有直接实现媒体文件的加载和渲染,而是把这些工作委托给了在创建播放器或者播放器准备好播放的时候注入的组件。所有ExoPlayer实现的通用组件有:
(1) ExoPlayer:创建ExoPlayer的实现实例
if (mExoPlayer == null) {
// 创建ExoPlayer的实例
mExoPlayer = ExoPlayerFactory.newSimpleInstance(this, DefaultRenderersFactory(this), DefaultTrackSelector(), DefaultLoadControl())
// 添加ExoPlayer的监听
mExoPlayer!!.addListener(this)
}
对于ExoPlayer来说,其创建实例的时候后面的几个参数是比较重要的,我们点击进去看下源码里面
public static SimpleExoPlayer newSimpleInstance(
Context context,
RenderersFactory renderersFactory,
TrackSelector trackSelector,
LoadControl loadControl) {
return newSimpleInstance(
context,
renderersFactory,
trackSelector,
loadControl,
/* drmSessionManager= */ null,
Util.getLooper());
}
参数1:Context;对应的是上下文实例,这个没什么好说的。
参数2:RenderersFactory;渲染器,用于渲染媒体文件。当创建播放器的时候,Renderers被注入。
参数3:TrackSelector ;轨道选择器,用于选择MediaSource提供的轨道(tracks),供每个可用的渲染器使用。
参数4:LoadControl ;用于控制MediaSource何时缓冲更多的媒体资源以及缓冲多少媒体资源。
ExoPlayer库提供了在普通使用场景下上述组件的默认实现。ExoPlayer可以使用这些默认的组件,也可以使用自定义组件。例如可以注入一个自定义的LoadControl用来改变播放器的缓存策略,或者可以注入一个自定义渲染器以使用Android本身不支持的视频解码器。
(2) MediaSource:媒体资源
用于定义要播放的媒体,加载媒体,以及加载媒体的路径,具体的写法为:
val uri = Uri.parse(mList!![mPlayBackPosition])
val mediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSourceFactory("exoplayer-codelab")).createMediaSource(uri)
// 设置好MediaSource之后通过prepare进行注入
mExoPlayer!!.prepare(mediaSource)
注意Uri.parse()里面设置的值可以是本地音频的路径也可以是网络音频的路径。
了解了ExoPlayer的基本信息后,接下来我们看看到底应该怎么去使用。
ExoPlayer的使用
第一步:添加依赖
implementation 'com.google.android.exoplayer:exoplayer:2.10.5'
为了省事,我们依赖了整个ExoPlayer库。你也可以只依赖你真正需要的库。例如果你要播放DASH类型的媒体资源,你可以只依赖Core,DASH,UI
这三个库。即
implementation 'com.google.android.exoplayer:exoplayer-core:2.10.5'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.10.5'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.10.5'
整个ExoPlayer库包括5个子库,依赖了整个ExoPlayer库和依赖5个子库是等效的。
exoplayer-core:核心功能 (必要)
exoplayer-dash:支持DASH内容
exoplayer-hls:支持HLS内容
exoplayer-smoothstreaming:支持SmoothStreaming内容
exoplayer-ui:用于ExoPlayer的UI组件和相关的资源。
第二步:创建ExoPlayer的实例
private var mExoPlayer: ExoPlayer? = null
if (mExoPlayer == null) {
mExoPlayer = ExoPlayerFactory.newSimpleInstance(this, DefaultRenderersFactory(this), DefaultTrackSelector(), DefaultLoadControl())
mExoPlayer!!.addListener(this)
}
第三步:创建媒体资源类MediaSource并装载资源文件
val uri = Uri.parse(mList!![mPlayBackPosition])
val mediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSourceFactory("exoplayer-codelab")).createMediaSource(uri)
mExoPlayer!!.prepare(mediaSource)
第四步:开始播放
mExoPlayer!!.playWhenReady = true
需要强调的是ExoPlayer的播放并没有像MediaPlayer一样通过很明显的start()、pause()、stop()
方法去实现,而是统一使用void setPlayWhenReady(boolean playWhenReady)
方法来实现,true代表的就是播放/继续播放
,false代表的就是暂停/停止
。
至此,一个完整的播放流程就结束了,当然除此之外还有一些比较重要的知识点,如下所示。
prepare(MediaSource mediaSource):装载资源文件
getDuration():获取流媒体的总播放时长,单位是毫秒。
setPlayWhenReady(boolean playWhenReady):播放/暂停/继续播放
seekTo(long positionMs):设置当前ExoPlayer的播放位置,单位是毫秒。
isPlaying():当前是否在播放
stop():停止播放
getCurrentPosition():获取当前ExoPlayer的播放位置,单位是毫秒。
getRepeatMode():获取当前是否循环播放
release():释放播放资源
setRepeatMode(int value):设置是否循环播放,可供取值为(1)Player.REPEAT_MODE_ONE(不允许循环播放,只会播放一次)
(2)Player.REPEAT_MODE_ALL(循环播放)
setPlaybackParameters:设置播放倍速
PlaybackParameters playbackParameters = new PlaybackParameters(speed, 1.0F);
mExoPlayer.setPlaybackParameters(playbackParameters);
当然,除了列举的这些API,还有其他的很多的API,这就需要我们平时在使用的过程中去把握了。
interface EventListener {
//播放总时间线改变,这里可用于设置播放总时长
default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
}
//播放资源有改变
default void onStaticMetadataChanged(List<Metadata> metadataList) {}
//是否在加载
default void onIsLoadingChanged(boolean isLoading) {
onLoadingChanged(isLoading);
}
//播放器播放状态改变,查看 State 有IDLE,BUFFERING加载中, READY 资源准备好, ENDED 已结束
default void onPlaybackStateChanged(@State int state) {}
//视频资源准备好就播放的设置改变
default void onPlayWhenReadyChanged(
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {}
//播放状态改变,开始播放或暂停
default void onIsPlayingChanged(boolean isPlaying) {}
//重复播放的模式改变
default void onRepeatModeChanged(@RepeatMode int repeatMode) {}
default void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {}
//播放器报错
default void onPlayerError(ExoPlaybackException error) {}
//参数改变
default void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {}
default void onEvents(Player player, Events events) {}
}
知识点三:ExoPlayer的回收
使用完ExoPlayer需要回收资源。ExoPlayer是很消耗系统资源的,所以在使用完ExoPlayer,不要等待系统自动回收,最好是主动回收资源。
if (mExoPlayer!= null) {
mExoPlayer.stop();
mExoPlayer.release();
mExoPlayer= null;
}
其实在实际开发中,我们应该将播放的逻辑与Service相结合,这里为了测试效果就没有写的那么完整
/**
* 框架ExoPlayer制作简单的媒体播放器
*/
class MainActivity : AppCompatActivity(), Player.EventListener {
private var mList: MutableList<String>? = null
private var mExoPlayer: ExoPlayer? = null
private var mPlayWhenReady: Boolean = false
private var mPlayBackPosition = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initMusic()
initializePlayer()
}
/**
* 加载音频文件
*/
private fun initMusic() {
mList = ArrayList()
mList!!.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/2e76a133b898a2f6c8fb62e963b87ce4_tongkuercanlandeyisheng.mp3")
mList!!.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/e845a564e795c628c6bdd652ddbc24e3_fulidadeqingshaonianshidai.mp3")
mList!!.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/e9b9abdcc3855586ec2c4651293c11e9_abuyuwulei.mp3")
mList!!.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/ba2eaf007614fdf81c8d9e895ba88ff2_abuchuangzuodekaishi.mp3")
}
/**
* 初始化媒体播放器
*/
private fun initializePlayer() {
if (mExoPlayer == null) {
mExoPlayer = ExoPlayerFactory.newSimpleInstance(this, DefaultRenderersFactory(this), DefaultTrackSelector(), DefaultLoadControl())
mExoPlayer!!.addListener(this)
}
// 创建一个音频文件
val uri = Uri.parse(mList!![mPlayBackPosition])
val mediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSourceFactory("exoplayer-codelab")).createMediaSource(uri)
mExoPlayer!!.prepare(mediaSource)
mExoPlayer!!.playWhenReady = mPlayWhenReady
}
/**
* 释放媒体资源
*/
private fun releasePlayer() {
if (mExoPlayer != null) {
mExoPlayer!!.stop()
mExoPlayer!!.release()
mExoPlayer = null
}
}
/**
* 上一首
*/
fun lastMusic(view: View) {
releasePlayer()
mPlayWhenReady = true
if (mPlayBackPosition == 0) {
mPlayBackPosition = mList!!.size - 1
} else {
mPlayBackPosition--
}
initializePlayer()
}
/**
* 播放
*/
fun startMusic(view: View) {
mPlayWhenReady = true
mExoPlayer!!.playWhenReady = mPlayWhenReady
}
/**
* 暂停
*/
fun pauseMusic(view: View) {
mPlayWhenReady = false
mExoPlayer!!.playWhenReady = mPlayWhenReady
}
/**
* 结束
*/
fun stopMusic(view: View) {
mPlayWhenReady = false
mExoPlayer!!.stop()
mExoPlayer!!.release()
}
/**
* 下一首
*/
fun nextMusic(view: View) {
releasePlayer()
mPlayWhenReady = true
if (mPlayBackPosition == mList!!.size - 1) {
mPlayBackPosition = 0
} else {
mPlayBackPosition++
}
initializePlayer()
}
/**
* 播放状态监听
*/
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
var stateString: String? = null
if (playWhenReady && playbackState === Player.STATE_READY) {
Log.d("zhoufan", "onPlayerStateChanged: actually playing media")
}
stateString = when (playbackState) {
Player.STATE_IDLE -> "ExoPlayer.STATE_IDLE -"
Player.STATE_BUFFERING -> "ExoPlayer.STATE_BUFFERING -"
Player.STATE_READY -> {
"ExoPlayer.STATE_READY -"
}
Player.STATE_ENDED -> "ExoPlayer.STATE_ENDED -"
else -> "UNKNOWN_STATE -"
}
Log.d("zhoufan", "changed state to $stateString playWhenReady: $playWhenReady")
}
}