在画布与绘制的部分就已经学习了如何实现简易的视频播放、暂停、停止

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);
    }