1·在Service中实例化MusicPlayer,实现对整个播放过程的控制##

上一次做到了找到音乐数据,并封装成对象装在ArrayList里,把数据的信息显示在UI上。下面一个阶段就要开始真正的音乐播放器的制作了。做音乐播放器之前首先要想到的就是用什么来做这个部分。于是我查阅了Android Developers官网的一些有关内容,果不其然的确有方便的API供开发者使用。这就是MediaPlayer这个类。

用官网的话概括这个类就是这样:

MediaPlayer class can be used to control playback of audio/video files and streams.

这个类可以用来控制音视频的文件或者流的播放。

在用这个类之前一定要熟悉MediaPlayer对象的生命周期。也就是下面这个图。

废话不多说,这个图直接用原文档来解释就很清楚了。

State Diagram

Playback control of audio/video files and streams is managed as a state machine.

The diagram shows the life cycle and the states of a MediaPlayer object driven by the supported playback control operations.

The ovals represent the states a MediaPlayer object may reside in.

The arcs represent the playback control operations that drive the object state transition.

There are two types of arcs. The arcs with a single arrow head represent synchronous method calls,

while those with a double arrow head represent asynchronous method calls.

MediaPlayer State diagram

From this state diagram, one can see that a MediaPlayer object has the following states:

When a MediaPlayer object is just created using new or after reset() is called, it is in the Idle state; and after release() is called, it is in the End state. Between these two states is the life cycle of the MediaPlayer object.

There is a subtle but important difference between a newly constructed MediaPlayer object and the MediaPlayer object after reset() is called. It is a programming error to invoke methods such as getCurrentPosition(), getDuration(), getVideoHeight(), getVideoWidth(), setAudioStreamType(int), setLooping(boolean), setVolume(float, float), pause(), start(), stop(), seekTo(int), prepare() or prepareAsync() in the Idle state for both cases. If any of these methods is called right after a MediaPlayer object is constructed, the user supplied callback method OnErrorListener.onError() won't be called by the internal player engine and the object state remains unchanged; but if these methods are called right after reset(), the user supplied callback method OnErrorListener.onError() will be invoked by the internal player engine and the object will be transfered to the Error state.

It is also recommended that once a MediaPlayer object is no longer being used, call release() immediately so that resources used by the internal player engine associated with the MediaPlayer object can be released immediately. Resource may include singleton resources such as hardware acceleration components and failure to call release() may cause subsequent instances of MediaPlayer objects to fallback to software implementations or fail altogether. Once the MediaPlayer object is in the End state, it can no longer be used and there is no way to bring it back to any other state.

Furthermore, the MediaPlayer objects created using new is in the Idle state, while those created with one of the overloaded convenient create methods are NOT in the Idle state. In fact, the objects are in the Prepared state if the creation using create method is successful.

In general, some playback control operation may fail due to various reasons, such as unsupported audio/video format, poorly interleaved audio/video, resolution too high, streaming timeout, and the like. Thus, error reporting and recovery is an important concern under these circumstances. Sometimes, due to programming errors, invoking a playback control operation in an invalid state may also occur. Under all these error conditions, the internal player engine invokes a user supplied OnErrorListener.onError() method if an OnErrorListener has been registered beforehand via setOnErrorListener(android.media.MediaPlayer.OnErrorListener).

It is important to note that once an error occurs, the MediaPlayer object enters the Error state (except as noted above), even if an error listener has not been registered by the application.

In order to reuse a MediaPlayer object that is in the Error state and recover from the error, reset() can be called to restore the object to its Idle state.

It is good programming practice to have your application register a OnErrorListener to look out for error notifications from the internal player engine.

IllegalStateException is thrown to prevent programming errors such as calling prepare(), prepareAsync(), or one of the overloaded setDataSource methods in an invalid state.

Calling setDataSource(FileDescriptor), or setDataSource(String), or setDataSource(Context, Uri), or setDataSource(FileDescriptor, long, long) transfers a MediaPlayer object in the Idle state to the Initialized state.

An IllegalStateException is thrown if setDataSource() is called in any other state.

It is good programming practice to always look out for IllegalArgumentException and IOException that may be thrown from the overloaded setDataSource methods.

A MediaPlayer object must first enter the Prepared state before playback can be started.

There are two ways (synchronous vs. asynchronous) that the Prepared state can be reached: either a call to prepare() (synchronous) which transfers the object to the Prepared state once the method call returns, or a call to prepareAsync() (asynchronous) which first transfers the object to the Preparing state after the call returns (which occurs almost right way) while the internal player engine continues working on the rest of preparation work until the preparation work completes. When the preparation completes or when prepare() call returns, the internal player engine then calls a user supplied callback method, onPrepared() of the OnPreparedListener interface, if an OnPreparedListener is registered beforehand via setOnPreparedListener(android.media.MediaPlayer.OnPreparedListener).

It is important to note that the Preparing state is a transient state, and the behavior of calling any method with side effect while a MediaPlayer object is in the Preparing state is undefined.

An IllegalStateException is thrown if prepare() or prepareAsync() is called in any other state.

While in the Prepared state, properties such as audio/sound volume, screenOnWhilePlaying, looping can be adjusted by invoking the corresponding set methods.

To start the playback, start() must be called. After start() returns successfully, the MediaPlayer object is in the Started state. isPlaying() can be called to test whether the MediaPlayer object is in the Started state.

While in the Started state, the internal player engine calls a user supplied OnBufferingUpdateListener.onBufferingUpdate() callback method if a OnBufferingUpdateListener has been registered beforehand via setOnBufferingUpdateListener(OnBufferingUpdateListener). This callback allows applications to keep track of the buffering status while streaming audio/video.

Calling start() has not effect on a MediaPlayer object that is already in the Started state.

Playback can be paused and stopped, and the current playback position can be adjusted. Playback can be paused via pause(). When the call to pause() returns, the MediaPlayer object enters the Paused state. Note that the transition from the Started state to the Paused state and vice versa happens asynchronously in the player engine. It may take some time before the state is updated in calls to isPlaying(), and it can be a number of seconds in the case of streamed content.

Calling start() to resume playback for a paused MediaPlayer object, and the resumed playback position is the same as where it was paused. When the call to start() returns, the paused MediaPlayer object goes back to the Started state.

Calling pause() has no effect on a MediaPlayer object that is already in the Paused state.

Calling stop() stops playback and causes a MediaPlayer in the Started, Paused, Prepared or PlaybackCompleted state to enter the Stopped state.

Once in the Stopped state, playback cannot be started until prepare() or prepareAsync() are called to set the MediaPlayer object to the Prepared state again.

Calling stop() has no effect on a MediaPlayer object that is already in the Stopped state.

The playback position can be adjusted with a call to seekTo(int).

Although the asynchronuous seekTo(int) call returns right way, the actual seek operation may take a while to finish, especially for audio/video being streamed. When the actual seek operation completes, the internal player engine calls a user supplied OnSeekComplete.onSeekComplete() if an OnSeekCompleteListener has been registered beforehand via setOnSeekCompleteListener(OnSeekCompleteListener).

Please note that seekTo(int) can also be called in the other states, such as Prepared, Paused and PlaybackCompleted state.

Furthermore, the actual current playback position can be retrieved with a call to getCurrentPosition(), which is helpful for applications such as a Music player that need to keep track of the playback progress.

When the playback reaches the end of stream, the playback completes.

If the looping mode was being set to truewith setLooping(boolean), the MediaPlayer object shall remain in the Started state.

If the looping mode was set to false , the player engine calls a user supplied callback method, OnCompletion.onCompletion(), if a OnCompletionListener is registered beforehand via setOnCompletionListener(OnCompletionListener). The invoke of the callback signals that the object is now in the PlaybackCompleted state.

While in the PlaybackCompleted state, calling start() can restart the playback from the beginning of the audio/video source.

可见这个MediaPlayer对象是非常耗时的。我们需要在一个Service里来实例化它。完成它的整个播放流程。

创建一个Service的子类,我把这个子类叫做MusicService.同时,我们必须要重写一些掌控Services生命周期的方法并提供一个结构化的组件来绑定到我们的MusicService上。下面就新建一个MusicService.java并重写方法。

我们这里的MusicService不对外提供绑定,不需要重写onStartCommand()方法,onBind()方法返回null即可。

同其他三大组件一样,需要在application's manifest file里声明。

AndroidManifest.xml

      <manifest ... >
...
<application ... >
<service
android:name="com.graceplayer.activity.MusicService"
android:exported="true" >
<intent-filter>
<action android:name="VideoService.START_Video_SERVICE" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
...
</application>
</manifest>

这样Service的基本框架就做好了。

在开始整个Service的编写操纵MediaPlayer对象的同时,引入广播机制,当MediaPlayer的对象在媒体播放引擎中的状态发生改变的时候,通过广播机制,将MediaPlayer对象的状态改变信息广播出去。同时设置给好Service绑定广播接收器,随时接受来自Acitvity的广播。通过广播中要求的信息,进行条件判断,做出相应的播放状态的改变。这样整个UI线程与后台耗时的媒体播放就完成了信息的交互。

根据这个需求,先给MusicService绑定一个广播接收器,专门接收来自UI的控制命令。这里用到了BroadCastReceiver这个类

在MusicService.java中封装成一个方法:(BROADCAST_MUSICSERVICE_CONTROL = "MusicService.ACTION_CONTROL"在完整的程序中会有声明)

MusicService.java

private void bindCommandReceiver() {

//new 一个CommandReceiver的对象,以接收到的广播数据为判断条件,完成对MediaPlayer对象的操作。

receiver = new CommandReceiver();

//过滤出我们想要接收的Intent

IntentFilter filter = new IntentFilter(BROADCAST_MUSICSERVICE_CONTROL);

//完成广播接收器注册,让receiver随时接收filter指定的广播

registerReceiver(receiver, filter);

}

//在MusicService中写一个继承自BroadcastReceiver的内部类,在这个类中对接收到的广播进行读取,根据接收到的广播的信息在

//MusicService中完成MediaPlayer对象的相应操作。我们先把MediaPlayer对象的具体操作抽象成方法。对方法先进行调用。

private class CommandReceiver extends BroadcastReceiver {

			@Override
public void onReceive(Context context, Intent intent) {
int command = intent.getIntExtra("command", COMMAND_UNKNOWN);
switch (command) {
case COMMAND_SEEK_TO:
seekTo(intent.getIntExtra("time", 0));
break;
case COMMAND_PLAY:
number = intent.getIntExtra("number", 0);
play(number);
break;
case COMMAND_PREVIOUS:
moveNumberToPrevious();
break;
case COMMAND_NEXT:
moveNumberToNext();
break;
case COMMAND_STOP:
stop();
break;
case COMMAND_RESUME:
resume();
break;
case COMMAND_CHECK_IS_PLAYING:
isPlaying();
default:
break;
}
} }

MusicService.java

	package com.zharma.greatlovemusic;

	import com.zharma.data.MusicList;

	import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.MediaPlayer;
import android.os.IBinder; public class MusicService extends Service{ // 播放控制命令,标识操作
public static final int COMMAND_UNKNOWN = -1;
public static final int COMMAND_PLAY = 0;
public static final int COMMAND_PAUSE = 1;
public static final int COMMAND_STOP = 2;
public static final int COMMAND_RESUME = 3;
public static final int COMMAND_PREVIOUS = 4;
public static final int COMMAND_NEXT = 5;
public static final int COMMAND_CHECK_IS_PLAYING = 6;
public static final int COMMAND_SEEK_TO = 7; // 播放器状态
public static final int STATUS_PLAYING = 0;
public static final int STATUS_PAUSED = 1;
public static final int STATUS_STOPPED = 2;
public static final int STATUS_COMPLETED = 3; // 广播标识
public static final String BROADCAST_MUSICSERVICE_CONTROL = "MusicService.ACTION_CONTROL";
public static final String BROADCAST_MUSICSERVICE_UPDATE_STATUS = "MusicService.ACTION_UPDATE"; // 广播接收器
private CommandReceiver receiver;
private int status; // 媒体播放类
private MediaPlayer player = new MediaPlayer(); //歌曲序号,从0开始
private int number = 0; @Override
public IBinder onBind(Intent intent) {
return null;
} @Override
public void onCreate() {
super.onCreate();
//绑定广播接收器,专门负责接收UI传过来的播放命令,判断条件并处理
bindCommandReceiver();
status = MusicService.STATUS_STOPPED; } @Override
public void onDestroy() {
//在Destory整个Service之前要释放掉MidiaPlayer对象调用的资源。
if(player != null) {
player.release();
}
super.onDestroy();
} private void bindCommandReceiver() {
receiver = new CommandReceiver();
IntentFilter filter = new IntentFilter(BROADCAST_MUSICSERVICE_CONTROL);
registerReceiver(receiver, filter);
} private class CommandReceiver extends BroadcastReceiver { @Override
public void onReceive(Context context, Intent intent) {
int command = intent.getIntExtra("command", COMMAND_UNKNOWN);
switch (command) {
case COMMAND_SEEK_TO:
seekTo(intent.getIntExtra("time", 0));
break;
case COMMAND_PLAY:
number = intent.getIntExtra("number", 0);
play(number);
break;
case COMMAND_PREVIOUS:
moveNumberToPrevious();
break;
case COMMAND_NEXT:
moveNumberToNext();
break;
case COMMAND_STOP:
stop();
break;
case COMMAND_RESUME:
resume();
break;
case COMMAND_CHECK_IS_PLAYING:
isPlaying();
default:
break;
}
} }
//当MediaPlayer对象的状态发生改变的时候,比如从prepare状态变换到pause状态都要通知给UI,让UI的广播接收器接收处理。让UI
//得到变换
private void sendBroadcastOnStatusChanged(int status) {
Intent intent = new Intent(BROADCAST_MUSICSERVICE_UPDATE_STATUS);
intent.putExtra("status", status);
if (status != STATUS_STOPPED) {
intent.putExtra("time", player.getCurrentPosition());
intent.putExtra("duration", player.getDuration());
intent.putExtra("number", number);
intent.putExtra("musicName", MusicList.getMusicList().get(number).getMusicName());
intent.putExtra("musicArtist", MusicList.getMusicList().get(number).getMusicArtist());
}
sendBroadcast(intent);
} } 这样就做到了整个MusicService的主体编写。下面就把变换的细节实现到相应的方法里。
private void load(int number) { try {
player.reset();
player.setDataSource(MusicList.getMusicList().get(number).getMusicPath());
player.prepare();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 注册监听器
player.setOnCompletionListener(completionListener);
} OnCompletionListener completionListener = new OnCompletionListener() { @Override
public void onCompletion(MediaPlayer mp) {
if (player.isLooping()) {
replay();
} else {
sendBroadcastOnStatusChanged(MusicService.STATUS_COMPLETED);
}
}
}; private void moveNumberToNext() {
if (number == MusicList.getMusicList().size()-1) {
Toast.makeText(MusicService.this,"已经到达列表底部",Toast.LENGTH_SHORT).show();
} else {
number ++;
play(number);
}
} private void moveNumberToPrevious() {
if (number == 0) {
Toast.makeText(MusicService.this, "已经到达列表顶端", Toast.LENGTH_SHORT).show();
} else {
number --;
play(number);
}
} private void play(int number) {
if (player != null && player.isPlaying()) {
player.stop();
}
load(number);
player.start();
status = MusicService.STATUS_PLAYING;
sendBroadcastOnStatusChanged(MusicService.STATUS_PLAYING);
} private void pause() {
if (player.isPlaying()) {
player.pause();
status = MusicService.STATUS_PAUSED;
sendBroadcastOnStatusChanged(MusicService.STATUS_PAUSED);
}
} private void stop() {
if (status != MusicService.STATUS_STOPPED) {
player.stop();
sendBroadcastOnStatusChanged(MusicService.STATUS_STOPPED);
}
}
/** 恢复播放(暂停之后) */
private void resume() {
player.start();
status = MusicService.STATUS_PLAYING;
sendBroadcastOnStatusChanged(MusicService.STATUS_PLAYING);
} private void isPlaying() {
if (player != null && player.isPlaying()) {
//status = MusicService.STATUS_PLAYING;
sendBroadcastOnStatusChanged(MusicService.STATUS_PLAYING);
}
}
private void replay() {
player.start();
status = MusicService.STATUS_PLAYING;
sendBroadcastOnStatusChanged(MusicService.STATUS_PLAYING);
} /** 跳转至播放位置 */
private void seekTo(int time) {
player.seekTo(time);
status = MusicService.STATUS_PLAYING;
sendBroadcastOnStatusChanged(MusicService.STATUS_PLAYING);
}

整个Service的核心思想就是设置广播接收器,接收来自UI的控制命令的广播,根据接收到的数据做出对MediaPlayer对象的操作。这个过程中,intent对象起着信使的作用。通过它,完成了组件之间的通信。

Android小玩意儿-- 从头开发一个正经的MusicPlayer(二)的更多相关文章

  1. Android小玩意儿-- 从头开发一个正经的MusicPlayer(三)

    MusicService已经能够接收广播,通过广播接收的内容来做出相应的MediaPlayer对象的处理,包括播放,暂停,停止等,并当MediaPlayer对象的生命周期发生变化的时候,同样通过发送广 ...

  2. Android小玩意儿-- 从头开发一个正经的MusicPlayer(一)

    之前从未接触过音乐播放器这块东西的开发.今天偶然想做一个自己的音乐播放器.算是练练手.既然要做,就要做一个正儿八经的App.很多网上的资料也是模模糊糊,不是很全,现在开始,自己摸索着尝试着一步一步的做 ...

  3. 微信小程序实战--开发一个简单的快递单号查询

    功能如图: 虽然工作中只负责小程序后台开发,但是还是小程序开发产生了浓厚的兴趣,官方文档也是超级详细了 这里就简单做一个快递单号的查询: 新建一个page: 接着就可以写wxml了.这里用一个简单的i ...

  4. 用Visual Studio 2015 编写 MASM 汇编程序(二)从头开发一个Win32汇编程序

    一,建立一个VC的控制台类型的空工程: 1,从VS菜单中选择“文件”->“新建”->“项目”. 2,在新建项目中选择:“Visual c++”->"Win32"- ...

  5. 微信小程序——安装开发工具和环境【二】

    准备 开发工具下载 获取APPID 安装工具 安装 接受协议 选择安装位置 等待安装完成 安装完成 选择项目 选择小程序 填写信息 确定 无误后,点击确定进入开发页面 建立普通快速启动模板界面

  6. Android | 教你如何用代码开发一个拍照翻译小程序

    引子   想必有很多小伙伴喜欢外出旅游,能去海外玩一圈那是更好不过了,旅游前大家一定会对吃.穿.住.行.游玩路线做各种攻略,然后满怀期待的出发- 想象中的旅游   出发前,想象中的旅游目的地可能有漂亮 ...

  7. Android | 教你如何用华为HMS MLKit 图像分割 SDK开发一个证件照DIY小程序

    Android | 教你如何用华为HMS MLKit 图像分割 SDK开发一个证件照DIY小程序 引子   上期给大家介绍了如何使用如何用华为HMS MLKit SDK 三十分钟在安卓上开发一个微笑抓 ...

  8. 「1.0」一个人开发一个App,小程序从0到1,起航了

    古有,秦.齐.楚.赵.魏.韩.燕七国争雄:今有,微信.QQ.百度.支付宝.钉钉.头条.抖音七台争霸.古有,白起.李牧.王翦.孙膑.庞涓.赵奢.廉颇驰骋疆场:今有程序员1,程序员2,程序员3…编写代码. ...

  9. Android | 教你如何用华为HMS MLKit SDK 三十分钟在安卓上开发一个微笑抓拍神器

    Android | 只要三十分钟就可以在手机上开发一个微笑抓拍神器!!! 前言 前段时间Richard Yu在发布会上给大家介绍了华为HMS Core4.0,回顾发布会信息请戳: 华为面向全球发布HM ...

随机推荐

  1. MYSQL初级学习笔记七:MySQL中使用正则表达式!(视频序号:初级_44)

    知识点九:MySQL中使用正则表达式(44) (1):REGEXP‘匹配方式’: (2):常用匹配方式: 模式字符 ^ 匹配字符开始的部分 $ 匹配字符串结尾的部分 . 代表字符串中的任意一个字符,包 ...

  2. mount机制3-/etc/mtab

    这次查看fuse_mount_sys函数的执行过程,理解mount的各个阶段. 这个函数能够执行的前提是命令行使用root账户. 1. 首先,该函数仍然是主要使用 mount(const char * ...

  3. 并不对劲的bzoj3994:loj2185:p3327[SDOI2015]约数个数和

    题目大意 设d(x)为x的约数个数,\(t\)组询问,给定\(n,m\)(\(t,m,n\leq5*10^4\)),求$ \sum^n_{i=1}\sum^m_{j=1}d(i*j)$ 题解 假设\( ...

  4. module+standard library.py

    #导入模块 import sys sys.path sys.path.append('D:\program files\Python34\PyWorks') #hello.py文件路径 #不用appe ...

  5. bzoj4557 [JLoi2016]侦察守卫——DP

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=4557 见这位的博客:https://www.cnblogs.com/Narh/p/91403 ...

  6. 动态编译c#脚本(把c#当作脚本执行)

    csscript动态编译C#脚本 This document contains information about the CLR based scripting system CS-Script ( ...

  7. 移植tslib库出现selected device is not a touchscreen I understand的解决方法

    首发平台:微信公众号baiwenkeji 很多人在做触摸屏驱动实验,移植tslib库时,可能会出现错误提示“selected device is not a touchscreen I underst ...

  8. 【转】构建Maven项目自动下载jar包

    原文地址:https://blog.csdn.net/gfd54gd5f46/article/details/54973954 使用Maven 自动下载jar包 右键单击项目,将项目 转换成Maven ...

  9. vue项目中的路径别名

    每次写引入组件的路径,如果路径嵌套比较深,那么会比较麻烦,我们可以在webpack.base.conf.js,中设置路径的别名,默认webpack设置src的别名为@ 建议配置src下一级目录的别名, ...

  10. CSS counter计数器(content目录序号自动递增)详解

    一.CSS计数器三角关系 CSS计数器只能跟content属性在一起的时候才有作用,而content属性貌似专门用在before/after伪元素上的.于是,就有了,“计数器↔伪元素↔content属 ...