序
在画布与绘制的部分就已经学习了如何实现简易的视频播放、暂停、停止
https://blog.csdn.net/nishigesb123/article/details/89468251#t7
而这部分关于安卓中的多媒体播放,利用这部分知识,可以实现一个“真正”的媒体播放器。
媒体播放概述
- Android的多媒体框架包括支持播放多种常见的媒体类型,使您可以轻松地把音频、视频和图像集成到你的应用。
- 你可以播放音频或视频媒体文件,这些文件应当是存储在你的应用程序的资源文件中的。
- 应用程序的资源文件可以是文件系统中独立的文件,或通过网络连接获取的一个数据流,所有使用MediaPlayer APIS的资源文件。
- 你只能在标准输出设备上播放音频数据,目前标准输出设备是移动设备的扬声器或耳机,你不能在谈话音频(电话)调用期间播放声音文件。
安卓多媒体播放涉及的内容有:(包括但不仅仅)
-
MediaPlayer(播放声音和视频的类,本篇的重点)
-
AudioManager(管理音频和音频输出设备)
-
SurfaceView(即开头提到的)
-
VideoView
-
TextureView
-
当然还有若干的第三方框架
可能需要的权限有:(包括但不仅仅)本篇及后续文章默认添加了下述权限~
- Internet许可——如果你使用的是基于网络内容的流媒体播放器,你的应用程序必须请求访问网络
<uses-permission android:name="android.permission.INTERNET"/>
-
如果你的播放应用需要阻止屏幕变暗或阻止处理器睡眠,或使用MediaPlayer.setScreenOnWhilePlaying()或 MediaPlayer.setWakeModel方法你必须请求此权限
<uses-permission android:name="android.permission.WAKE_LOCK"/>
- 读取外部存储(SdCard)权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
MediaPlayer
概述
安卓多媒体框架中最重要的组件之一就是Mediaplayer类。
- Mediaplayer类的对象通过少量的设置即能获取。
- Mediaplayer类的对象可以解码和播放音视频。
- Mediaplayer类的对象支持多种媒体源,如本地资源、内部URI(ContentResolver取得的URI)、外部URI(流媒体)
播放
播放本地资源中的音频
需要在res目录下创建raw目录,并放入对应的音频文件
//播放本地资源文件
public void playFromRes(View v) {
MediaPlayer mp = MediaPlayer.create(this, R.raw.test);
mp.start();
}
播放本地URI
需要在sdcard目录下的Music目录下加入对应的资源文件,并且需要读取外部存储器权限
//播放系统文件
public void playFromSys(View v) {
MediaPlayer mp = new MediaPlayer();
String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getPath() + "/test.wav";
try {
//设置数据源
mp.setDataSource(this, Uri.parse(path));
mp.prepare();
mp.start();
} catch (IOException e) {
e.printStackTrace();
}
}
播放网络URI
这里注意:
preparel()调用可能很耗时,因为它可能需要获取并打开解码媒体数据,会执行很长时间,此时你就不能从你的应用的UI线程中调用它,这会导致U挂起,选择prepareAsync()替代它。
需要在配置清单加入网络权限
//播放网络资源文件
public void playFromNet(View v) {
String path = "http://gddx.sc.chinaz.net/Files/DownLoad/sound1/201902/11213.wav";
MediaPlayer mp = new MediaPlayer();
try {
mp.setDataSource(this, Uri.parse(path));
mp.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
}
});
//异步缓冲 调用立即开始用另一个线程取缓存
mp.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
}
效果
测试效果就没办法了,都是音频,截个图意思意思
管理状态
MediaPlayer有一个内部的状态,特定的操作只能在特定的状态时才有效。如果你在错误的状态下执行一个操作,系统可能抛出一个异常或导致一个意外的行为。
本节内容所有图片出处:https://blog.csdn.net/shulianghan/article/details/38487967
MediaPlayer状态图例
Idle状态 和 End状态
Error状态
Initalized 状态
Prepared和 Preparing状态
Started状态
Paused状态
Stopped 状态
播放位置调整
PlaybackCompleted状态
释放MediaPlayer
- MediaPlayer可能消耗大量的系统资源。
- 因此你应该总是采取一些额外的措失来确保在一个MediaPlayer实例上不会桂起太长的时间。
- 当你用完MediaPlayer时,你应该总是调用releasel()来保证任何分配给MediaPlayer的系统资源被正确地释放。
mediaPlayer.release();
mediaPlayer = null;
简单的播放器实现案例
布局文件
核心是四个按钮及对应点击事件,图片仅仅是装饰品
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.395" />
<Button
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/last"
android:onClick="last"
android:text="上一首"/>
<Button
app:layout_constraintStart_toEndOf="@id/last"
app:layout_constraintEnd_toStartOf="@id/pause"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/play"
android:onClick="play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="播放"/>
<Button
app:layout_constraintStart_toEndOf="@id/play"
app:layout_constraintEnd_toStartOf="@id/next"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/pause"
android:onClick="pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂停"/>
<Button
app:layout_constraintStart_toEndOf="@id/pause"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/next"
android:onClick="next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下一首"/>
</android.support.constraint.ConstraintLayout>
MainActivity
用到了读取外部存储,所以需要给对应的权限。
主要实现
- View.OnClickListener
- MediaPlayer.OnPreparedListener
- MediaPlayer.OnErrorListener
- MediaPlayer.OnCompletionListener
package com.example.a4_24musicplayer;
import android.media.MediaPlayer;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
//简单播放器
public class MainActivity extends AppCompatActivity implements View.OnClickListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
private MediaPlayer mp;
//表示当前要播放音乐的索引 0即第一首
private int index = 0;
//音乐文件列表(存音乐文件地址)
private ArrayList<String> musicList = new ArrayList<>();
//按钮
private Button button_play, button_pause, button_next, button_last;
//是否处于暂停播放状态
private boolean isPause = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initMusic();
mp = new MediaPlayer();
//注册
mp.setOnPreparedListener(this);
mp.setOnErrorListener(this);
mp.setOnCompletionListener(this);
}
private void initMusic() {
String root = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getPath();
//separator系统的默认名称分隔符
musicList.add(root + File.separator + "a1.mp3");
musicList.add(root + File.separator + "a2.mp3");
musicList.add(root + File.separator + "a3.mp3");
musicList.add(root + File.separator + "a4.mp3");
}
private void initView() {
button_play = findViewById(R.id.play);
button_pause = findViewById(R.id.pause);
button_next = findViewById(R.id.next);
button_last = findViewById(R.id.last);
button_play.setOnClickListener(this);
button_pause.setOnClickListener(this);
button_next.setOnClickListener(this);
button_last.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.play:
play();
break;
case R.id.pause:
pause();
break;
case R.id.next:
next();
break;
case R.id.last:
last();
break;
default:
break;
}
}
//开始播放
private void play() {
if (isPause) {
mp.start();
isPause = false;
} else {
restart();
}
}
//从头开始播放音乐
private void restart() {
if (index < musicList.size()) {
//如果正在播放就停止
if (mp.isPlaying()) mp.stop();
mp.reset();
String musicPath = musicList.get(index);
try {
mp.setDataSource(musicPath);
mp.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//暂停播放
private void pause() {
if (mp.isPlaying()) {
mp.pause();
isPause = true;
}
}
//播放上一首
private void last() {
if (index - 1 >= 0) {
index--;
} else {
index = musicList.size() - 1;
}
restart();
}
//播放下一首
private void next() {
if (index + 1 < musicList.size()) {
index++;
} else {
index = 0;
}
restart();
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
mp.reset();
return true;
}
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
}
@Override
public void onCompletion(MediaPlayer mp) {
next();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mp != null) {
if (mp.isPlaying()) mp.stop();
mp.release();
}
}
}
使用服务控制MediaPlayer
如果你希望你的媒体在你的应用不出现在屏幕上时仍能在后台播放。
即,你希望当用户与其它应用交互时仍能继续播放,那么你必须启动一个Service并且通过它来控制MediaPlayer实例。
先介绍一个可能用到的东西。
使用唤醒锁
- 在后台播放媒体的应用时,当你的service正在运行时,设备可能进入休眠,因为Android系统在休眠时会试着节省电能。
- 那么系统会试着关闭电话的任何不必要的特牲,包括CPU和WiFi。
- 然而,如果你的service正在播放或接收音乐,你就应该阻止系统干涉你的播放工作。
- 为了在上述情况下保证你的service继续运行,你必须使用"wakelocks"。
- wakelock是一种通知系统在手机空闲时也应为你的应用保留所用特性的途径。
注意:你应该保守的使用wakelocks,并且仅在真证需要时才持有它,因为它们会显著的减少设备电池的寿命
需要权限
<uses-permission android:name="android.permission.WAKE_LOCK"/>
实现
创建一个MusicService
package com.example.a4_24service_mediaplayer;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayer;
import android.net.wifi.WifiManager;
import android.os.Environment;
import android.os.IBinder;
import android.os.PowerManager;
import java.io.File;
import java.io.IOException;
public class MusicService extends Service implements MediaPlayer.OnPreparedListener {
//将操作定义成公共常量
public static final String ACTION_PLAY = "com.example.ACTION_PLAY";
public static final String ACTION_PAUSE = "com.example.ACTION_PAUSE";
public static final String ACTION_EXIT = "com.example.ACTION_EXIT";
private WifiManager.WifiLock lock;
private MediaPlayer mediaPlayer;
public MusicService() {
}
@Override
public void onCreate() {
super.onCreate();
//创建实例
mediaPlayer = new MediaPlayer();
//注册
mediaPlayer.setOnPreparedListener(this);
//保持CPU正常工作,设置模式
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
//保持WIFI达到不被休眠
WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
lock = wifiManager.createWifiLock("mylock");
//得到锁
lock.acquire();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
//判断操作
if (ACTION_PLAY.equals(action)) {
mediaPlayer.reset();
try {
mediaPlayer.setDataSource(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC) + File.separator + "a1.mp3");
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
} else if (ACTION_PAUSE.equals(action)) {
if (mediaPlayer.isPlaying()) mediaPlayer.pause();
} else if (ACTION_EXIT.equals(action)) {
if (mediaPlayer.isPlaying()) mediaPlayer.stop();
mediaPlayer.release();
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
//释放锁
lock.release();
}
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
}
MainActivity
package com.example.a4_24service_mediaplayer;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void play(View v) {
Intent intent = new Intent(this, MusicService.class);
intent.setAction(MusicService.ACTION_PLAY);
startService(intent);
}
public void pause(View v) {
Intent intent = new Intent(this, MusicService.class);
intent.setAction(MusicService.ACTION_PAUSE);
startService(intent);
}
public void exit(View v) {
Intent intent = new Intent(this, MusicService.class);
intent.setAction(MusicService.ACTION_EXIT);
startService(intent);
}
}
需要在配置清单里进行注册
并且需要权限
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
通知作为前台服务
在MusicService服务里也进行一些修改
创建一个notification(),并且在onCreate()调用。
private void notification() {
Notification.Builder builder = new Notification.Builder(this);
builder.setTicker("音乐播放器");
builder.setSmallIcon(R.mipmap.test);
builder.setContentTitle("我的音乐播放器");
builder.setContentInfo("正在播放");
PendingIntent pi = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pi);
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = builder.build();
startForeground(0, notification);
nm.notify(0,notification);
}
最后onDestroy()的时候删除前台通知
@Override
public void onDestroy() {
super.onDestroy();
stopForeground(true);
//释放锁
lock.release();
}
效果如👇
在安卓8.0及以上在通知栏的政策有一定修改。
可以参考https://blog.csdn.net/u010231682/article/details/80732879
提供了一种新版本的写法,可以参考👇
private void notification() {
NotificationChannel channel = new NotificationChannel("Notify","Notify", NotificationManager.IMPORTANCE_LOW);
Notification.Builder builder = new Notification.Builder(this,"Notify");
builder.setTicker("音乐播放器");
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentTitle("我的音乐播放器");
builder.setContentInfo("正在播放");
PendingIntent pi = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pi);
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = builder.build();
startForeground(0, notification);
nm.notify(0,notification);
}