第十一章 多媒体应用开发(音视频播放)

第十一章 多媒体应用开发

第一节 音视频开发

Android SDK 提供了简单的 API 来播放音频、视频,从而让大部分的 Android应用程序开发者无需为音频和视频的一些底层操作而担忧。Android 提供了常见音频、视频的编码、解码机制,Android 支持的音频格式较多,常见的有 MP3、WAVE、Ogg 和 3GP 等,支持的视频格式常见的有 MP4 和 3GP 等。在 Android 操作系统之中,开发者直接使用 MediaPlayer 类就可以完成音频或视频文件的播放操作。

1、 MediaPlayer 类

MediaPlayer是处于 Android 多媒体包下"android.media.MediaPlayer"的 Android 自带的多媒体库, 一般我们用于实现音频视频(视频需要和其他控件一起使用比如: SurfaceViewVideoView 等)的操作

1.1使用 MediaPlayer 播放音频
1.创建MediaPlayer对象,并装载音频文件

① 可以使用直接new的方式:

MediaPlayer mp = new MediaPlayer();
//要用setDataSource()方法指定要播放的资源文件。并且在调用start()方法之前需要调用prepare ()。
//装载文件
player=new MediaPlayer();
try {
 player.setDataSource(this,Uri.**parse**("android.resource://" + getPackageName() + "/" + R.raw.**da**));
 player.prepare();
} catch (IOException e) {
 e.printStackTrace();
}

② 也可以使用create的方式,如:

MediaPlayer mp = MediaPlayer.create(this, R.raw.test);
//这时就不用调用setDataSource了,直接用player.start(),不需要也不能在start()方法之前调用prepare()方法
2.设置要播放的文件

MediaPlayer的setDataSource一共四个方法:

  • void setDataSource(String path): 指定装载path路径所代表的文件。

  • void setDataSource(FileDescriptor fd, long offset, long length):指定装载fd所代表的文件中从offset开始、长度为length的文件内容。

  • void setDataSource(FileDescriptor fd): 指定装载fd所代表的的文件。

  • void setDataSource(Context context,Uri uri): 指定装载URI代表的文件。

在使用setDataSource()方法装载音频文件后,实际上MediaPlayer并未真正装载该音频文件,还需要调用MediaPlayer的prepare ()方法真正装载音频件

MediaPlayer要播放的文件主要包括3个来源:

播放应用中事先自带的resource资源

res文件夹与asset文件夹的区别?

Android 中资源分为两种,一种是res下可编译的资源文件, 这种资源文件系统会在R.Java里面自动生成该资源文件的ID,访问也很简单,只需要调用R.xxx.id即可;第二种就是放在assets文件夹下面的原生资源文件,放在这个文件夹下面的文件不会被R文件编译,所以不能像第一种那样直接使用;并且res/raw不可以有目录结构,而assets则可以有目录结构,也就是assets目录下可以再建立文件夹

A. 播放应用的资源文件

​ i. 调用MediaPlayercreate(Context context , int resid)方法加载指定资源文件。

​ ii. 调用MediaPlayerstart()、pause ()、stop()等方法控制播放即可。

音频资源文件一般放在Android应用的/res/raw目录下

例如:MediaPlayer.create(this, R.raw.song);

或者使用SetDataSource()

Uri uri=Uri.parse("android.resource://"+this.getPackageName()+"/"+R.raw.da);

   player=new MediaPlayer();

   try {

​     player.setDataSource(this,uri);

​     player.prepare();

   } catch (IOException e) {

​     e.printStackTrace();

   }

B. 播放应用的原始资源文件

​ i. 调用Activity中的getAssets ()方法获取应用的AssetManager。

​ ii. 调用AssetManager对象的openFd(String name)方法打开指定的原生资源,该方法返回一个AssetFileDescriptor对象。

​ //获取通过openFd()的方法获取asset目录下指定文件的AssetFileDescriptor对象。

​ iii. 调用AssetFileDescriptor的getFileDescriptor()、getStartOffset()和getLength()方法来获取音频文件的FileDescriptor、开始位置、长度等。

​ //在AssetManager中一项的文件描述符。这提供你自己打开的FileDescriptor可用于读取的数据,以及在文件中的偏移量和长度的该项的数据。

​ iv. 创建MediaPlayer对象,并调用MediaPlayer对象的setDataResource(FileDescriptor fd , long offset,long length )方法来装载音频资源。

​ v. 调用MediaPlayer对象的prepare ()方法准备音频。

​ vi. 调用MediaPlayer的start()、pause()、stop()等方法控制播放即可

存储在SD卡或其他文件路径下的媒体文件

​ i. 创建MediaPlayer对象,并调用MediaPlayer对象的setDataSource(String path)方法装载指定音频文件。

​ ii. 调用MediaPlayer对象的prepare()方法准备音频。

​ iii. 调用MediaPlayer的start()、stop()等方法控制播放即可。

例如:player=new MediaPlayer();
   try {
		player.setDataSource("/sdcard/pm.mp3");	
//或者
player.setDataSource(Environment.getExternalStorageDirectory ()+"/pm.mp3");
​    player.prepare();
   } catch (IOException e) {
​    e.printStackTrace();
   }

注意:对SDCard访问需要在AndroidManifest.xml设置相应权限

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

安卓23.0版本以上,不仅仅要设置上面的权限,还要在对SD卡有读写操作的地方授权

//判断是否6.0以上的手机
if(Build.VERSION.SDK_INT>=23){
//判断是否有这个权限
int permission = ActivityCompat.checkSelfPermission (this, Manifest.permission.READ_EXTERNAL_STORAGE);
//checkSelfPermission检查对应权限是否打开,如果是PackageManager.PERMISSION_GRANTED 表示打开
if (permission != PackageManager.PERMISSION_GRANTED) {
  // 请求权限
  ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_REQ_CODE);
}
//第二个参数表示请求的权限数组
//第三个参数表示的请求码,请求码要求大于0

网络上的媒体文件

A. 直接使用create(Context context,Uri uri)

例如:MediaPlayer mp=MediaPlayer.create(this,Uri.parse(http://www.test.cn/music/m1.mp3);

B. 调用setDataSource(Context context,Uri uri)

例如:mp.setDataSource(this,Uri.parse("http://www.test.cn/music/m1.mp3"));

注:要在AndroidManifest.xml文件中授予程序访问网络的权限,具体代码如下:

<users-permission android:name="android.permission.INTERNET"/>
3.对播放器的主要控制方法:

Android通过控制播放器的状态的方式来控制媒体文件的播放,其中:prepare()和prepareAsync() 提供了同步和异步两种方式设置播放器进入prepare状态

  • MediaPlayer中的prepare方法和prepareAsync方法的区别

prepare ()

同步地为播放器的回放做准备。设置数据源和显示表面之后,需要调用prepare()或prepareAsync()。对于文件,可以调用prepare(),它会阻塞,直到MediaPlayer准备好播放为止。

prepareAsync ()

异步地为播放器的回放做准备。设置数据源和显示表面之后,需要调用prepare()或prepareAsync()。对于流,应该调用prepareAsync(),它会立即返回,而不是阻塞,直到缓冲了足够的数据。

prepare方法是将资源同步缓存到内存中,一般加载本地较小的资源可以用这个,如果是较大的资源或者网络资源建议使用prepareAsync方法

  • 对于异步装载考虑是能否在资源装载完成后才可以操作呢?

需要设置监听事件setOnPreparedListener();来通知MediaPlayer资源已经获取到了,然后实现onPrepared(MediaPlayer mp)方法.在里面启动MediaPlayer

videoView.setOnPreparedListener(p);
OnPreparedListener p = new OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.play();
	}
};
  • start():开始播放或恢复已经暂停的音频的播放

  • stop():停止正在播放的音频

  • pause():暂停正在播放的音频

  • void seekTo(int msec):设置当前MediaPlayer的播放位置,可以让播放器从指定的位置开始播放,单位是毫秒。

  • int getDuration():获取流媒体的总播放时长,单位是毫秒。

  • int getCurrentPosition():获取当前流媒体的播放的位置,单位是毫秒。

  • void setLooping(boolean looping):设置是否循环播放。播放完毕后,不会回调OnCompletionListener,而是从头播放当前音频。

  • boolean isLooping():判断是否循环播放。

  • boolean isPlaying():判断是否正在播放。

  • void release ():回收流媒体资源。

  • void setAudioStreamType(int streamtype):设置播放流媒体类型。

  • void setWakeMode(Context context, int mode):设置CPU唤醒的状态。

  • setNextMediaPlayer(MediaPlayer next):设置当前流媒体播放完毕,下一个播放的MediaPlayer。

4.MediaPlayer的监听事件

通常在新建一个MediaPlayer实体后,会对给它增加需要的监听事件

  • MediaPlayer.OnPreparedListener:MediaPlayer进入准备完成的状态触发,表示媒体可以开始播放了。

  • MediaPlayer.OnSeekCompleteListener:调用MediaPlayer的seekTo方法后,MediaPlayer会跳转到媒体指定的位置,当跳转完成时触发。需要注意的时,seekTo并不能精确的跳转,它的跳转点必须是媒体资源的关键帧。

  • MediaPlayer.OnBufferingUpdateListener:网络上的媒体资源缓存进度更新的时候会触发。比如我们加载网络音频的时候,常用这个监听器来监听缓冲进度

  • MediaPlayer.OnCompletionListener:媒体播放完毕时会触发。由于player占有的系统资源比较大,因此在播放结束后,就应该调用该方法,把player占有的资源给释放掉。但是当OnErrorLister返回false,或者MediaPlayer没有设置OnErrorListener时,这个监听也会被触发。

  • MediaPlayer.OnVideoSizeChangedListener:视频宽高发生改变的时候会触发。当所设置的媒体资源没有视频图像、MediaPlayer没有设置展示的holder或者视频大小还没有被测量出来时,获取宽高得到的都是0.

  • MediaPlayer.OnErrorListener:MediaPlayer出错时会触发,无论是播放过程中出错,还是准备过程中出错,都会触发。

1.2 MediaPlayer的状态图和及生命周期

在这里插入图片描述

MediaPlayer状态
  • Idle状态以及End状态

    • 在MediaPlayer创建实例或者调用reset函数后,播放器就被创建了,这个时候就是一定处于Idle状态。

    • 任何时候调用 relase()函数后,就会变成 End状态

    • 在这两个状态之间的就是MediaPlayer的生命周期

  • Error状态

    • 在idle状态时候调用 start、pause、stop、seekTo等函数,都会触发OnErrorListener.onError,使MediaPlayer处于 onError状态;

    • 当MediaPlayer处于onError状态时,它将不能再被调用,因为本次生命周期已经结束了。

    • 一旦有错误,MediaPlayer就会进入到Error状态,要用 reset()函数才能重新建立MediaPlayer,这个时候就会回到Idle状态了。

  • Initalized状态(初始化)

    • 当调用setDataSource()函数时,MediaPlayer的Idle状态就会变成 Initalized状态。

    • 如果setDataSource在非Idle状态时调用,会抛出 IllegalStateException 异常

  • Prepared状态

    • new一个MediaPlayer,处于Idle状态。如果用create方法创建实例,当创建完成时处于Prepared状态。

    • MediaPlayer在开始播放音频前必须处于Prepared状态

    • MediaPlayer有两种途径到达Prepared状态

同步方法:使用本地音视频文件,调用 prepare() 同步的将 lnitalized状态变为Prepared状态

异步方法:使用网络音视频,需要缓冲;调用 prepareAsync()异步的将 lnitalized状态变为 Preparing状态,最后再到Prepared状态 ,Preparing状态是个短暂的状态

Preparing状态是一个过渡状态(transient state),处于Prepared状态时,可以通过相对应的方法设置音量,屏幕常亮,播放循环等。

  • Started状态

    • 调用 start()进入到Started状态,调用start()返回成功后,MediaPlayer处于Started状态。可以通过isPlaying()来判断当前是否在 Started状态;

    • 如果MediaPlayer已经处于Started状态,再调用start()是没有任何用的。

  • Paused状态

    • 当调用 paused()时,MediaPlayer 瞬间从Started变为Prepared状态,但是在播放器内部,这个过程是异步的。播放音频流的时候,这个转换过程可能会需要几秒钟。

    • 如果在Paused状态下调用 start(), playback()会恢复之前暂停时的位置,接着开始播放,这时候又变成了 Started状态

    • 当然了,如果已经是Paused状态,调用paused()是没有任何用处的。

  • Stopped状态

    • 当调用stop()时,MediaPlayer无论处于哪种状态都会进入Stopped状态

    • 在Stopped状态时,必须先调用prepare() 或 prepareAsync ()进入Prepared状态后,才能播放音频。

    • 如果已经处于Stopped状态时,调用 stop()是没有任何用处的

  • PlaybackCompleted状态

    • 音频播放完成后,播放完毕。

    • 可以通过 getCurrentPosition()获取播放的位置

    • 当MediaPlayer播放结束时:

    • 如果设置了setLooping(true),MediaPlayer依然处于Started状态

    • 如果设置了setLooping(false),并且事先重写过setOnCompletionListener,播放器会回调上面那个接口的 onCompletion(),然后进入 PlaybackCompleted(播放完成)状态。

    • 当处于PlaybackCompleted时,调用start()将重启播放器从头开始播放数据

2、AudioEffect类

提升音频效果的方法总体分为软件和硬件两大类。硬件如:HiFi耳机、smartPA、音源修复算法、音效算法等等。软件层面,可以大致分为基于音频数据流的处理方式以及基于Android的音效框架的处理方式。基于音频数据流的处理方式比较容易理解,基本原理就是先把数据丢给第三方的库进行处理,接着将处理完成的数据重新写入原有数据流的节点即可。这里主要探讨下基于Android的音效框架的处理方式。AudioEffect就是android audio framework(android 音频框架)

AudioEffect的具体效果作用在音频数据上,MediaPlayer只管播放音频,二者通过AudioSessionId(通过MediaPlayer的getAudioSessionId()方法可以获得)关联起来。可以说,音效的处理对MediaPlayer是透明的,具体的处理由Android框架进行。

在这里插入图片描述

2.1 AudioEffect是音效控制基类,开发者不应直接使用此类,应该使用它的派生类:
  • AcousticEchoCanceler:回声消除器

  • AutomaticGainControl:自动增强控制器

  • NoiseSuppressor:噪音抑制器

  • Equalizer均衡器:增加或降低某一频率的声音响度来达到想要的效果

  • Visualizer频谱(示波器):音频频谱可视化

  • Virtualizer:环绕音

  • BassBoost重低音控制器:增加低音的强度

  • PresetReverb预设混响(推荐用于音乐):使音乐通过声音在不同路径传播下造成的反射叠加产生的声音特效,比如流行,古典,爵士等。

  • EnvironmentalReverb环境混响(推荐用于游戏):比如马路,走廊,室内,大厅等

以上音效包含在android.media.audiofx包中

当创建AudioEffect时,如果音频效果应用到一个具体的AudioTrack和MediaPlayer的实例,应用程序必须指定该实例的音频session ID,如果要应用Global音频输出混响的效果必须制定Session 0.

要创建音频输出混响(音频 Session 0)要求要有 MODIFY_AUDIO_SETTINGS权限

2.2 BassBoost: 重低音调节器
  • 通过放大音频中的低频音来实现重低音特效,相当于一个只能对低频声音进行增益控制的简单均衡器

  • Android中给重低音调节器设置了1000个级别,也就是当我们的seekBar的最大值就是1000

重低音调节器的用法

1. 初始化

private BassBoost mBass =new BassBoost(0, mMediaPlayer.audioSessionId)

第一个参数代表该音效控制器的优先级,这里设置为0,第二个参数仍然是MediaPlayer的id

2. 启用

为了启用它们同样需要调用setEnabled(true)方法

3. 调用相关方法

BassBoost的常用方法如下:

  • getRoundedStrength () :获取特效力度,特效力度值在0~1000间变化

  • setStrength(short value) :设置特效力度

2.3 Virtualizer环绕音

环绕音依赖于输入和输出通道的数量和类型,需要打开立体声通道。通过放置音源于不同的位置,环绕音完美地再现了声音的质感和饱满感。

1. 初始化

Virtualizer mVirtualizer= new Virtualizer (0, mMediaPlayer.getAudioSessionId()); //优先级为0

如果指定的会话ID为0,则Virtualizer作用于主要的音频输出混音器(mix)。

2. 启用

为了启用它们同样需要调用setEnabled(true)方法

3. 调用相关方法

Virtualizer的常用方法如下:

  • getRoundedStrength() //获取特效力度,特效力度值在0~1000间变化

  • setStrength() //设置特效力度

Virtualizer mVirtualizer= new Virtualizer(0,mPlayer.getAudioSessionId()); //优先级为0

if (mVirtualizer.getStrengthSupported()){
short strength = mVirtualizer.getRoundedStrength(); mVirtualizer.setStrength((short)strength);
}
2.4 Equalize均衡器

均衡器的原理:调整某一频率声音的分贝。所以我们在使用均衡器之前需要先获取当前系统支持的所有可调整的频率,例如有些手机目前仅支持60、230、910、3600、14000Hz这些频率。

1. 初始化

Equalizer mEqualizer = new Equalizer(0, mPlayer.getAudioSessionId()); //优先级为0

2. 启用

为了启用它们同样需要调用setEnabled(true)方法

3. 调用相关方法

Equalizer的常用方法如下:

  • getNumberOfBands() //获取均衡器支持的总频率数

  • getCenterFreq () //根据索引来获取频率

  • setBandLevel ()//为某个频率的均衡器设置参数

//获取能够设置的最小和最大分贝数
val minLevel = mEqualizer.bandLevelRange[0]
val maxLevel = mEqualizer.bandLevelRange[1]
```

2.5 预设混响处理: PresetReverb

一个声音在产生后会往各个方向传播。如果这个声音是在室内产生的,接收方会首先听到音源本身发出的声音,然后听到来自墙壁、天花板和地板的回声。这些回声又会在传播到障碍物上时产生二次回声、三次回声…接收方会持续不断地听到声音,这每一次声音的辨识度越来越低,且响度随时间衰减。混响器(Reverb)对于音频接收者的环境建模非常重要,它可以被用于模拟在不同环境中播放音乐产生的效果,或者在游戏中为玩家生成更加沉浸式的音频体验。APP 可以通过预设混响器提前配置全局的混响参数,这种方式在为音乐播放添加混响效果时被广泛使用。如果 APP 需要配置更高级的环境声混响参数,最好使用环境声混响器来实现。

1. 初始化

PresetReverb  mPresetReverb = new PresetReverb(0, mPlayer.getAudioSessionId());

//同时要初始化一个均衡器和一个或多个List用于存储预设音场的名字和值

2. 启用

为了启用它们同样需要调用setEnabled(true)方法

3. 获取所有预设名字getNumberOfPresets()

4. 使用Spinner作为选择工具,并且在选择监听中使用均衡器(预设音场控制器只定义了音场,具体实现需要均衡器)

5. 设定音场

mEqualizer.usePreset(list.get(i))
2.6 环境声混响器: EnvironmentalReverb

如果想要有很好的混响效果可以使用EnvironmentalReverb

EnvironmentalReverb常用方法:

  • setDecayHFRatio: 设置高频到中频衰减比率。范围是[100, 2000] ,如果设为1000,则全部衰减相同。

  • setDecayTime: 中频混响衰减时间。[100, 20000]

  • setDensity: 在后期混响衰减,控制模态密度的值。[0, 1000]

  • setDiffusion: 在后期混响衰减,控制回声密度的值。 [0, 1000]

  • setReflectionsDelay: 初始反射延迟时间。[0, 300]

  • setReflectionsLevel: 对于环境效果的早期反射等级。[-9000, 1000]

2.7 Visualizer:示波器

示波器是用来显示波形的控件,但是这不是一个View,它只是提供了当前的波形信息(byte数组),我们需要自定义View去显示

首先需要一个权限

<uses-permission android:name="android.permission.RECORD_AUDIO"/>

Visualizer 有两个比较重要的方法

  • 设置每次捕获频谱的大小, getCaptureSizeRange()所返回的数组里面有两个值,数组[0]是最小值(128),数组[1]是最大值(1024)

  • 设置可视化数据的采集频率范围0~getMaxCaptureRate()

Visualizer通过一个监听器来监听所采集的数据

mVisualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener(),Visualizer.getMaxCaptureRate() / 2, true, false){}

一共有4个参数,第一个参数是监听者,第二个参数表示的是采集的频率,第三个参数表示是否采集波形,第四个参数表示是否采集频率

OnDataCaptureListener 有2个回调,一个用于显示FFT数据,展示不同频率的振幅,另一个用于显示声音的波形图

对mVisualizer的许多设置必须在setEnable之前完成

  • AcousticEchoCanceler:取消回声控制器

  • AutomaticGainControl:自动增益控制器

  • NoiseSuppressor:噪音压制控制器

注意:并不能保证所有的设备都能支持这些效果的,所以应该首先调用在对应音频效果类上的isAvailable()的方法来检测它的可用性。

例如:AcousticEchoCanceler.isAvailable();

上述三个子类的操作步骤:

  • 调用它们的静态方法create()方法创建相应的实例,

  • 调用它们的isAvailabel()方法判断是否可用,

  • 调用setEnable(boolean enabled)方法启用相应的效果即可

3、VolumeShaper类

VolumeShaper在音频应用中使用淡入,淡出和交叉淡入淡出以及其他短暂的自动音量转换。VolumeShaper类是在Android8.0中新增的功能。

使用VolumeShaper进行音量控制实际是通过 VolumeShaper.Configuration来实现的。

3.1 VolumeShaper.Configuration

VolumeShaper.Configuration的三个参数

  • 音量曲线

    • 音量曲线表示随时间的幅度变化。它由一对浮点数组定义,x []和y []定义了一系列控制点。每个(x,y)对分别代表时间和时间点对应的音量比例值。两个数组必须具有相同的长度并且包含至少2个且不超过16个值。

    • 时间坐标在区间[0.0,1.0]内给出。第一个时间点必须是0.0,最后一个时间点必须是1.0,时间必须单调递增。音量比例值在区间[0.0,1.0]内以线性刻度指定。

  • 插补器类型

    音量曲线始终通过指定的控制点,根据配置的内插器类型,控制点之间的值由样条曲线导出。

    可用VolumeShaper插补器类型有四个常量 :

    • VolumeShaper.Configuration.INTERPOLATOR_TYPE_STEP:使用分段曲线的方式控制音量变化

    • VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR:使用线性的插值方式控制音量变化

    • VolumeShaper.Configuration.INTERPOLATOR_TYPE_CUBIC:使用三次方曲线的插值方式控制音量变化

    • VolumeShaper.Configuration.INTERPOLATOR_TYPE_CUBIC_MONOTONIC:使用保持局部单调的三次曲线的插值方式控制音量变化

  • 持续时间

    时间间隔[0.0,1.0]中的指定时间坐标缩放到指定的持续时间(以毫秒为单位)。这决定了运行时音量曲线的实际时间长度,并将曲线应用于音频输出。

3.2 使用VolumeShaper

1.创建配置

在构建之前VolumeShaper,必须创建一个实例VolumeShaper.Configuration。

VolumeShaper.Configuration config =new VolumeShaper.Configuration.Builder()
  .setDuration(3000)
  .setCurve(new float[] {0.f, 1.f}, new float[] {0.f, 1.f})
  .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR).build();

**如果没有参数,**VolumeShaper.Configuration.Builder构造函数将返回一个构建器,该构建器使用默认设置创建一个配置:INTERPOLATOR_TYPE_CUBIC,持续时间为1秒,并且不包含曲线。在调用之前,您必须向构建器添加曲线build()。

2.创建一个VolumeShaper

要创建一个VolumeShaper,调用createVolumeShaper(),传入VolumeShaper.Configuration对象

volumeShaper = myMediaPlayer.createVolumeShaper(config);

VolumeShaper不能在MediaPlayer之间共享,但可以调用相同配置来创建多个VolumeShaper应用于不同的MediaPlayer

3.运行VolumeShaper

​ 创建VolumeShaper后,第一次调用apply()必须指定PLAY 操作进行启动

shaper.apply(VolumeShaper.Operation.PLAY);

改变曲线:使用replace()方法来更改VolumeShaper曲线。

4、使用SoundPool播放音频

由于MediaPlayer占用资源较多,且不支持同时播放多个音频,所以Android还提供了另一个播放音频的类——SoundPool。SoundPool即音频池,可以同时播放多个短小的音频,如果超过流的最大数目,SoundPool会基于优先级自动停止先前播放的流,而且占用的资源较少。另外SoundPool还支持自行设置声音的品质、 音量、 播放比率等参数。

4.1 SoundPool相对于MediaPlayer的优点
  1. SoundPool适合短且对反应速度比较高的情况(游戏音效或按键声等),文件大小一般控制在几十K到几百K,最好不超过1M

  2. SoundPool可以与MediaPlayer同时播放,SoundPool也可以同时播放多个声音;

  3. SoundPool最终编解码实现与MediaPlayer相同;

  4. MediaPlayer只能同时播放一个声音,加载文件有一定的时间,适合文件比较大,响应时间要是那种不是非常高的场景

4.2 SoundPool 的创建

SoundPool 的创建方式在不同版本中会有所不同,为了更好的兼容性,应该对API版本进行判断,再对应的进行创建。

在5.0以前,直接使用它的构造方法即可

SoundPool (int maxStreams, int streamType, int srcQuality)
  • maxStreams 同时播放流的最大数量,当播放的流的数目大于此值,则会选择性停止优先级较低的流

  • streamType 指定声音类型,流类型可以分为STREAM_VOICE_CALL(语音电话声音), STREAM_SYSTEM(系统的声音), STREAM_RING(电话铃声的声音),STREAM_MUSIC(手机音乐的声音) 和 STREAM_ALARM(手机闹铃的声音)五种类型。在AudioManager中定义。

  • srcQuality 指定声音品质(采样率变换质量),例如音乐CD可能采用44.1KHZ的16位立体声,默认填充0

而在这之后,则需要使用Builder模式来创建

SoundPool.Builder spb = new SoundPool.Builder()
.setMaxStreams(16)
.setAudioAttributes (audioAttributes)
.build();

AudioAttributes(音频属性)

AudioAttributes {

  mUsage

  mContentType

  mSource

  mFlags

  mTags / mFormattedTags / mBundle (key value pairs)

}

使用Builder模式来构造

AudioAttributes attr = new AudioAttributes.Builder()      //设置音效相关属性.setUsage(AudioAttributes.USAGE_GAME)         // 设置音效使用场景.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)  // 设置音效的类型.build();
  • setUsage用来设置用途(比如是游戏还是媒体)

  • setContentType用来设置内容类型(如电影、语音或提示音/铃声)

4.3 加载所要播放的音频

创建SoundPool对象后,可以调用load()方法来加载要播放的音频。load()方法的语法格式有以下4种。

  • public int load (Context context, int resId, int priority):用于通过指定的资源ID来加载音频。

  • public int load (String path, int priority):用于通过音频文件的路径来加载音频。

  • public int load (AssetFileDescriptor afd, int priority):用于从AssetFileDescriptor所对应的文件中加载音频。

  • public int load (FileDescriptor fd, long offset, long length, int priority):用于加载FileDescriptor对象中从offset开始,长度为length的音频。

上述方法都会返回一个声音的ID,后面我们可以通过这个ID来播放指定的声音

priority:没什么用的一个参数,建议设置为1,保持和未来的兼容性

为了更好地管理所加载的每个音频,一般使用HashMap<Integer, Integer>对象来管理这些音频。这时可以先创建一个HashMap<Integer, Integer>对象,然后应用该对象的put()方法将加载的音频保存到该对象中。例如,创建一个HashMap<Integer, Integer>对象,并应用put()方法添加一个音频

4.4 播放音频

调用SoundPool对象的play()方法可播放指定的音频。

  • play (int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate)

  • soundID:用于指定要播放的音频,该音频为通过load()方法返回的音频

  • leftVolume:用于指定左声道的音量,取值范例为0.0~1.0

  • rightVolume:用于指定右声道的音量,取值范例为0.0~1.0

  • priority:用于指定播放音频的优先级,数值越大,优先级越高

  • loop:用于指定循环次数,0为不循环,-1为循环

  • rate:用于指定速率,正常为1,最低为0.5,最高为2

创建完 SoundPool 后,通过setOnLoadCompleteListener设置监听,用来监听资源加载完毕的事件发生。这主要是为了播放做准备.通过名字可猜测到, 当音频资源加载完成后,会回调设置的监听的onLoadComplete方法, 在这个方法里, 可以进行播放音频

soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {@Overridepublic void onLoadComplete(SoundPool soundPool, int voiceId, int status) {if (status == 0) {
​          soundPool.play(voiceId, 1, 1, 1, 0, 1);}}});
4.5 播放控制

streamID是对流的特定实例的引用,通过 SoundPool 的 pause\resume\stop 方法, 就可以对播放进行控制。

  • pause方法用来暂停指定mStreamID的流 (暂停播放)

  • resume方法用来恢复指定mStreamID的流 (恢复播放)

  • stop方法用来停止指定mStreamID的流 (停止播放)

4.6 播放音量调节

通过 SoundPool 的 setVolume 方法就可以设置指定 mStreamID 的流的左右声道的音量值

mSoundPool.setVolume(mStreamID, mCruLeftVolume, mCurRightVolume);
4.7 资源释放

通过unload 方法来卸载之前load的资源 , 并通过 release 方法释放SoundPool占用的资源

4.8 注意事项

尽管 SoundPool 使用比较简单,但是还是有许多需要注意的地方:

  • 并不是所有的音频格式都支持,可能有些格式是不支持的

  • 尽量使用占用内存较小的音频文件,时长也不要过长,因为 SoundPool 也只会播放很短的时间

  • 加载资源的数目也需要注意(256限制),过多可能会导致问题

  • 播放之前一定要确保资源以及加载完毕了,否则可能会出现异常情况

  • 设置流的优先级的问题,当同时播放的活动流的数目超过设置的maxStreams值的时候,会根据优先级来停止优先级较低的流,如果有多个具有相同低优先级的流,它将选择要停止的最旧流,并且该流不再有效;如果要播放的流的优先级最低,则会播放失败,返回的streamID为零ID

  • 可以设置播放的重复次数,当设置为-1 则表示会无限循环下去,此时若要停止,需要显示的调用stop() 方法

  • 可以设置播放速率,设置范围为0.5 ~ 2.0 , 正常为 1.0

5、VideoView类

VideoView是Android主要的视频播放View,它其实是对MediaPlayer的再次封装;VideoView 底层仍然是使用 MediaPlayer 来对视频文件进行控制。

5.1 VideoView的基本操作步骤

首先需要在布局文件中添加该组件,然后在Activity中获取该组件,并应用其setVideoPath()方法或setVideoURI()方法加载要播放的视频,最后调用start()方法来播放视频。另外,VideoView组件还提供了stop()和pause()方法,用于停止或暂停视频的播放。

  1. 在布局文件中添加 VideoView组件

基本语法格式:

<VideoView

​    android:id="@+id/video_view"

​    android:layout_width="wrap_content"

​    android:layout_height="wrap_content"

​    app:layout_constraintTop_toTopOf="parent"

​    app:layout_constraintBottom_toBottomOf="parent"

​    app:layout_constraintLeft_toLeftOf="parent"

​    app:layout_constraintRight_toRightOf="parent"/>

2.调用VideoView的两个方法来加载指定视频。

  • setVideoPath(String path):以文件路径的方式设置 VideoView 播放的视频源。。

  • setVideoURI(Uri uri): 以 Uri 的方式设置 VideoView 播放的视频源,可以是网络 Uri 或本地 Uri。

  //获取raw下的文件   
videoView.setVideoURI(Uri.parse("android.resource:// lmh.example.exam1_6 /" + R.raw.video));
  //获取外部存储下的文件   
videoView.setVideoPath(Environment.getExternalStorageDirectory() + "/big_buck.mp4");
  //     访问网络视频   
videoView.setVideoPath("http://www.test.com/big_buck.mp4");

3.调用ViedoView的start()、stop()、pause()方法来控制视频播放。

  • void start():开始播放,视频资源未绑定就绪,或者已经在播放时,再次 start 不会有影响

  • void pause():暂停,视频资源未绑定就绪,或者已经暂停时,再次 pause不会有影响

  • void stopPlayback():停止播放,并释放资源

    其他方法

  • resume:将视频从头开始播放

  • seekTo:从指定的位置开始播放视频

  • isPlaying:判断当前是否正在播放视频

  • getCurrentPosition:获取当前播放的位置

  • getDuration:获取载入的视频文件的时长

  • setVideoPath(String path):以文件路径的方式设置VideoView播放的视频源

  • setVideoURI(Uri uri):以Uri的方式设置视频源,可以是网络Uri或本地Uri

  • setMediaController(MediaController controller):设置MediaController控制器

  • setOnCompletionListener(MediaPlayer.onCompletionListener -):监听播放完成的事件

  • setOnErrorListener(MediaPlayer.OnErrorListener -):监听播放发生错误时候的事件

  • setOnPreparedListener(MediaPlayer.OnPreparedListener -):监听视频装载完成的事件

5.2 MediaControl 组件

MediaController是Android封装的辅助控制器,带有暂停,播放,停止,进度条等控件。通过VideoView+MediaController可以很轻松的通过图形控制界面实现视频播放、停止、快进、快退等功能。

  • MediaController的常用方法

    • public void setMediaPlayer (MediaController.MediaPlayerControl player):设置媒体播放器的对象。

    • public void setAnchorView (View view):设置这个控制器绑定(anchor/锚)到一个视图上。例如可以是一个VideoView对象,或者是你的activity的主视图。

    • public void show ():在屏幕上显示这个控制器。它将在3秒以后自动消失。

    • public void show (int timeout):在屏幕上显示这个控制器。它将在闲置’超时 (timeout)’毫秒到达后自动消失,如果设置为0将一直显示到调用hide()函数为止

    • public void hide():隐藏媒体控制条。

    • public boolean isShowing:判断媒体控制条是否正在显示。

    • public void setPrevNextListeners (View.OnClickListener next, View.OnClickListener prev):设置前一个按钮与后一个按钮的点击监听器。

注意: setMediaPlayer和setAnchorView只能同时调用其中一个方法。

6、使用 MediaPlayer和SurfaceView播放视频

6.1 SurfaceView

SurfaceView继承之View,但拥有独立的绘制表面,即它不与其宿主窗口共享同一个绘图表面,可以单独在一个线程进行绘制,并不会占用主线程的资源。这样,绘制就会比较高效,游戏,视频播放,还有最近热门的直播,都可以用SurfaceView

SurfaceView和普通View的区别

  • Android中,每个Activity对应一个显示图层,这个Activity里的所有控件都是显示在这个图层中。而SurfaceView跟其它控件不一样,它单独对应一个图层,跟Activity平级。但是在层次排列上,SurfaceView的图层在Activity图层的下面,所以需要在Activity图层上显示一块透明的区域,用于显示下面的SurfaceView图层。

  • 传统View及其派生类的更新只能在UI线程,然而UI线程还同时处理其他交互逻辑,这就无法保证View更新的速度和帧率了,而SurfaceView可以用独立的线程进行绘制,因此可以提供更高的帧率,例如游戏,摄像头取景等场景就比较适合SurfaceView来实现。

6.2 SurfaceHolder
  • SurfaceHolder是一个接口,其作用就像一个关于Surface的监听器,提供访问和控制SurfaceView内嵌的Surface相关的方法。

  • 在SurfaceView中有一个方法getHolder,可以很方便地获得SurfaceView内嵌的Surface所对应的监听器接口SurfaceHolder,通过这个对象管理图层的绘制、刷新等操作。

public SurfaceHolder getHolder()   
6.3 SurfaceView与MediaPlayer结合
  1. SurfaceView的作用:

    • SurfaceView主要用于显示MediaPlayer播放的视频流媒体的画面渲染

    • 软件解析视频流的步骤:首先它需要先确定视频的格式,这个和解码相关,不同的格式视频编码不同。知道了视频的编码格式后,再通过编码格式进行解码,最后得到一帧一帧的图像,并把这些图像快速的显示在界面上,即为播放一段视频。而SurfaceView在Android中就是完成这个功能的。

    • 既然SurfaceView是配合MediaPlayer使用的,MediaPlayer也提供了相应的方法设置SurfaceView显示图片,只需要为MediaPlayer指定SurfaceView显示图像即可。

    • void setDisplay(SurfaceHolder sh),它需要传递一个SurfaceHolder对象,SurfaceHolder可以理解为SurfaceView装载需要显示的一帧帧图像的容器。

  2. SurfaceView双缓冲

    • SurfaceView和大部分视频应用一样,把视频流解析成一帧帧的图像进行显示,但是如果把这个解析的过程放到一个线程中完成,可能在上一帧图像已经显示过后,下一帧图像还没有来得及解析,这样会导致画面的不流畅或者声音和视频不同步的问题。

    • 什么是双缓冲呢?双缓冲可以理解为有两个线程轮番去解析视频流的帧图像,当一个线程解析完帧图像后,把图像渲染到界面中,同时另一线程开始解析下一帧图像,使得两个线程轮番配合去解析视频流,以达到流畅播放的效果。

在这里插入图片描述

​ SurfaceView内部实现了双缓冲的机制,但是实现这个功能是非常消耗系统内存的。因为移动设备的局限性,Android在设计的时候规定,SurfaceView如果为用户可见的时候,创建SurfaceView的SurfaceHolder用于显示视频流解析的帧图片,如果发现SurfaceView变为用户不可见的时候,则立即销毁SurfaceView的SurfaceHolder,以达到节约系统资源的目的。

​ 如果开发人员不对SurfaceHolder进行维护,会出现最小化程序后,再打开应用的时候,视频的声音在继续播放,但是不显示画面了的情况,这就是因为当SurfaceView不被用户可见的时候,之前的SurfaceHolder已经被销毁了,再次进入的时候,界面上的SurfaceHolder已经是新的SurfaceHolder了。所以SurfaceHolder需要我们开发人员去编码维护,维护SurfaceHolder需要用到它的一个回调,SurfaceHolder.Callback(),它需要实现三个如下三个方法:

  • void surfaceDestroyed(SurfaceHolder holder):当SurfaceHolder被销毁的时候回调。

  • void surfaceCreated(SurfaceHolder holder):当SurfaceHolder被创建的时候回调。

  • void surfaceChange(SurfaceHolder holder):当SurfaceHolder的尺寸发生变化的时候被回调。

以下是这三个方法的调用的过程,在应用中分别为SurfaceHolder实现了这三个方法,先进入应用,SurfaceHolder被创建,创建好之后会改变SurfaceHolder的大小,然后按Home键回退到桌面销毁SurfaceHolder,最后再进入应用,重新SurfaceHolder并改变其大小。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值