学而实习之 不亦乐乎

Android 音视频播放器 MediaPlayer 的使用

2023-09-06 21:06:27

Android 多媒体框架支持播放各种常见媒体类型,所以可以轻松地将音频、视频和图像集成到应用程序中。

一、基本用法

1、相关类

MediaPlayer:播放声音和视频,是播放声音和视频的主要API。
AudioManager:管理设备上的音频源和音频输出。

2、权限

Internet权限——如果您正在使用MediaPlayer来播放流基于网络的内容,那么您的应用程序必须请求网络访问。

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

Wake Lock权限——如果您的播放器应用程序需要阻止屏幕变暗或处理器休眠,或者使用mediaplayer . setscreenonwhile play()或MediaPlayer.setWakeMode()方法,您必须请求此权限。

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

3、使用

媒体框架最重要的组件之一是 MediaPlayer 类。这个类的对象可以使用最少的设置获取、解码和播放音频和视频。它支持几种不同的媒体来源,如:

  1. 本地资源
  2. 内部uri,例如您可能从contentProvider获得的uri
  3. 外部url(流) 有关Android支持的媒体格式列表,请参阅支持的媒体格式页面。

【1】下面是如何播放本地音频资源(保存在您的应用程序的res/raw/目录中):

MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // no need to call prepare(); create() does that for you

在本例中,“raw”资源是系统不尝试以任何特定方式解析的文件。然而,这个资源的内容不应该是原始音频。它应该是一个以支持的格式之一适当编码和格式化的媒体文件。

【2】下面是您如何从系统中本地可用的URI(例如,您通过内容解析器获得的URI)进行播放:

Uri myUri = ....; // initialize Uri here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();

【3】通过HTTP流媒体从远程URL播放如下:

String url = "http://........"; // your URL here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // might take long! (for buffering, etc)
mediaPlayer.start();

注意:
如果要通过一个URL来传输流媒体在线文件,该文件必须能够逐步下载。
在使用 setDataSource() 时,您必须捕获或传递 IllegalArgumentException 和 IOException,因为您引用的文件可能不存在。

二、使用进阶

使用 MediaPlayer 原则上很简单。但是,需要记住的是,要将它正确地集成到典型的 Android 应用程序中,还需要做一些其他的事情。如下:

1、异步准备 prepareAsync()

prepare()的调用可能需要很长时间执行,因为它可能涉及到获取和解码媒体数据。因此,就像任何需要很长时间才能执行的方法一样,永远不要从应用程序的UI线程调用它。这样做会导致UI挂起,直到方法返回,这是一种非常糟糕的用户体验,并可能导致ANR(应用程序没有响应)错误。即使您希望您的资源能够快速加载,也要记住,在UI中任何需要超过十分之一秒才能响应的内容都会引起明显的暂停,并给用户留下您的应用程序很慢的印象。

为了避免挂起 UI 线程,生成另一个线程来准备 MediaPlayer,并在完成时通知主线程。然而,虽然您可以自己编写线程逻辑,但是在使用 MediaPlayer 时,这种模式非常常见,因此框架提供了一种方便的方法来通过使用 prepareAsync() 方法来完成此任务。该方法开始在后台准备媒体并立即返回。当媒体完成准备工作时,将调用 通过setOnPreparedListener() 配置的 MediaPlayer.OnPreparedListener() 的 onPrepared() 方法。

2、管理状态

MediaPlayer 是基于状态的。也就是说,MediaPlayer 有一个内部状态,在编写代码时必须始终注意到,因为只有当player处于特定状态时,某些操作才有效。如果在错误的状态下执行操作,系统可能会抛出异常或引发其他不希望看到的行为。

MediaPlayer 类中的文档显示了一个完整的状态机,它阐明了哪些方法将 MediaPlayer 从一个状态移动到另一个状态。例如,当您创建一个新的 MediaPlayer 时,它处于空闲状态。这时,您应该通过调用 setDataSource() 来初始化它,使它处于初始化状态。之后,您必须使用 prepare() 或 prepareAsync() 方法来准备它。当 MediaPlayer 完成准备工作时,它进入准备状态,这意味着您可以调用 start() 来让它播放媒体。此时,您可以通过调用 start()、pause() 和seekTo() 等方法在 start、pause和PlaybackCompleted 状态之间切换。但是,当您调用 stop() 时,请注意,在重新准备MediaPlayer之前,您不能再次调用start()。

在编写与 MediaPlayer 对象交互的代码时,一定要记住状态图,因为从错误的状态调用其方法是导致错误的常见原因。如下图:


3、释放媒体播放器

MediaPlayer可能会消耗有价值的系统资源。因此,您应该始终采取额外的预防措施,以确保您没有过多地依赖 MediaPlayer 实例。处理完它之后,应该始终调用 release(),以确保分配给它的任何系统资源都被正确释放。例如,如果您使用的是一个媒体播放器和活动接收 onStop() 调用,您必须释放媒体播放器,因为当你的活动不与用户进行交互,继续持有实例毫无意义(除非你是在后台播放媒体,这是在下一节中讨论)。当您的活动恢复或重新启动时,当然,您需要创建一个新的MediaPlayer,并在恢复回放之前重新准备。如下代码:

mediaPlayer.release();
mediaPlayer = null;

作为一个例子,考虑一下如果您在活动停止时忘记释放 MediaPlayer,而在活动重新开始时创建一个新的,可能会发生的问题。正如你可能知道的,当用户更改屏幕的方向(或更改设备配置以另一种方式),系统处理,通过重新启动活动(默认情况下),所以你可能会很快消耗掉所有系统资源的用户旋转设备之间来回的肖像和风景,因为在每一个方向变化,您创建一个新的媒体播放器,你永远不会释放。

四、在服务中使用MediaPlayer

在用户离开活动时如果您想继续播放“背景媒体”,会发生什么,这与内置音乐应用程序的表现非常类似。在这种情况下,您需要的是一个由服务控制的 MediaPlayer。

如果想要媒体在后台播放,即使您的应用程序不是在屏幕上——也就是说,您想要它在用户与其他应用程序交互时继续播放——那么您必须启动一个服务并从那里控制 MediaPlayer 实例。需要将 MediaPlayer 嵌入到 MediaBrowserServiceCompat 服务中,并让它与另一个活动中的 MediaBrowserCompat 交互。

要小心这个 client/server 设置。人们对在后台服务中运行的播放器如何与系统的其他部分进行交互抱有期望。如果您的应用程序没有满足这些期望,用户可能会有一个糟糕的体验。

1、异步运行

首先,与活动一样,服务中的所有工作在默认情况下都是在单个线程中完成的——事实上,如果您从同一个应用程序运行活动和服务,默认情况下它们使用相同的线程(“主线程”)。因此,服务需要快速处理传入意图,并且在响应它们时从不执行冗长的计算。如果预期有任何繁重的工作或阻塞调用,您必须异步执行这些任务:要么从另一个您自己实现的线程执行,要么使用框架的许多异步处理工具。

例如,在使用主线程中的MediaPlayer时,应该调用prepareAsync()而不是prepare(),并实现MediaPlayer.OnPreparedListener目的是在准备完成后开始播放时得到通知。例如:

public class MyService extends Service implements MediaPlayer.OnPreparedListener {
    private static final String ACTION_PLAY = "com.example.action.PLAY";
    MediaPlayer mMediaPlayer = null;
​
    public int onStartCommand(Intent intent, int flags, int startId) {
        ...
        if (intent.getAction().equals(ACTION_PLAY)) {
            mMediaPlayer = ... // initialize it here
            mMediaPlayer.setOnPreparedListener(this);
            mMediaPlayer.prepareAsync(); // prepare async to not block main thread
        }
    }
​
    /** Called when MediaPlayer is ready */
    public void onPrepared(MediaPlayer player) {
        player.start();
    }
}

2、处理异步错误

在同步操作中,错误通常会以异常或错误代码发出信号,但无论何时使用异步资源,都应该确保将错误通知给应用程序。对于MediaPlayer,您可以通过实现MediaPlayer.OnErrorListener并将其设置到MediaPlayer实例中来解决该问题。

public class MyService extends Service implements MediaPlayer.OnErrorListener {
    MediaPlayer mMediaPlayer;
​
    public void initMediaPlayer() {
        // ...initialize the MediaPlayer here...
        mMediaPlayer.setOnErrorListener(this);
    }
​
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        // ... react appropriately ...
        // The MediaPlayer has moved to the Error state, must be reset!
    }
}

要记住,当发生错误时,MediaPlayer将切换到错误状态,您必须在再次使用它之前重置它。

3、使用唤醒锁(wake locks)

当设计在后台播放媒体的应用程序时,设备可能会在服务运行时休眠。由于Android系统试图在设备处于休眠状态时节省电池,所以系统试图关闭手机的任何不必要的功能,包括CPU和WiFi硬件。然而,如果您的服务正在播放或流媒体音乐,您希望防止系统干扰您的播放。

为了确保您的服务在这些条件下继续运行,您必须使用“唤醒锁”。唤醒锁是一种向系统发出信号的方式,即您的应用程序正在使用某些特性,即使手机处于空闲状态,这些特性也应该保持可用。

注意:你应该尽量少用唤醒锁,并且只在必要的时候使用它们,因为它们会大大减少设备的电池寿命。
要确保在MediaPlayer播放时CPU继续运行,在初始化MediaPlayer时调用setWakeMode()方法。一旦你这样做了,MediaPlayer会在播放时持有指定的锁,并在暂停或停止时释放锁:

mMediaPlayer = new MediaPlayer();
// ... other initialization here ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);


但是,在本例中获得的唤醒锁只保证CPU保持清醒。如果你是通过网络流媒体,使用的是Wi-Fi,你可能也想要一个WifiLock,你必须手动获取和释放它。因此,当您开始使用远程URL准备MediaPlayer时,您应该创建并获得Wi-Fi锁。例如:

WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
    .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");​
wifiLock.acquire();

当你暂停或停止你的媒体,或当你不再需要网络,你应该释放锁:

wifiLock.release();

4、执行清理

如前所述,MediaPlayer对象可能会消耗大量的系统资源,所以您应该只在需要的时候使用它,并且在使用之后调用release()。显式调用这种清理方法而不是依赖于系统垃圾收集是很重要的,因为垃圾收集器重新声明MediaPlayer可能需要一些时间,因为它只对内存需求敏感,而不缺乏其他与媒体相关的资源。因此,在使用服务时,您应该总是重写onDestroy()方法,以确保释放MediaPlayer:

public class MyService extends Service {
   MediaPlayer mMediaPlayer;
   // ...
​
   @Override
   public void onDestroy() {
       super.onDestroy()
       if (mMediaPlayer != null) mMediaPlayer.release();
   }
}

五、数码版权管理(DRM)

1、DRM

从Android 8.0 (API级别26)开始,MediaPlayer就包含了支持drm保护材料回放的API。它们类似于MediaDrm提供的低级API,但是它们在更高级别上操作,并且不公开底层提取器、drm和加密对象。

尽管MediaPlayer DRM API没有提供MediaDrm的全部功能,但它支持最常见的用例。当前实现可以处理以下内容类型:

  1. 受广泛保护的本地媒体文件
  2. 宽带保护远程/流媒体文件

下面的代码片段演示了如何在简单的同步实现中使用新的DRM MediaPlayer方法。
要管理drm控制的媒体,您需要在通常的MediaPlayer调用流之外包括新的方法,如下所示:

setDataSource();
setOnDrmConfigHelper(); // optional, for custom configuration
prepare();
if (getDrmInfo() != null) {
  prepareDrm();
  getKeyRequest();
  provideKeyResponse();
}
​
// MediaPlayer is now ready to use
start();
// ...play/pause/resume...
stop();
releaseDrm();

像往常一样,初始化MediaPlayer对象并使用setDataSource()设置其源。然后,要使用DRM,执行以下步骤:

如果您希望应用程序执行自定义配置,请定义OnDrmConfigHelper接口,并使用setOnDrmConfigHelper()将其附加到播放器上。

  • 调用prepare()。
  • 调用getDrmInfo()。如果源具有DRM内容,该方法将返回一个非空MediaPlayer.DrmInfo价值。

如果 MediaPlayer.DrmInfo 存在:

  • 检查可用uuid的映射并选择一个。
  • 通过调用prepareDrm()为当前源程序准备DRM配置。

如果您创建并注册了一个 OnDrmConfigHelper 回调,它将在 prepareDrm() 执行时被调用。这允许您在打开DRM会话之前执行DRM属性的自定义配置。在调用 prepareDrm() 的线程中同步调用回调函数。要访问 DRM 属性,可以调用 getDrmPropertyString() 和 setDrmPropertyString()。避免执行冗长的操作。

如果设备还没有提供好,那么prepareDrm()也会访问配置服务器来提供设备。这可能需要可变的时间,这取决于网络连接。

  • 要将不透明的键请求字节数组发送到许可证服务器,请调用 getKeyRequest()。
  • 要将从许可证服务器接收到的密钥响应通知DRM引擎,请调用 provideKeyResponse()。结果取决于键请求的类型:
    • 如果响应是脱机键请求,则结果是键集标识符。您可以通过restoreKeys()使用这个键集标识符将键恢复到新会话。
    • 如果响应是流请求或发布请求,则结果为空。


2、异步运行 prepareDrm()

默认情况下,prepareDrm()同步运行,阻塞直到准备工作完成。但是,在新设备上进行的第一次 DRM 准备也可能需要进行准备,准备工作由prepareDrm()内部处理,由于涉及网络操作,可能需要一些时间才能完成。通过定义和设置MediaPlayer.OnDrmPreparedListener,可以避免在prepareDrm()上阻塞。

当您设置OnDrmPreparedListener时,prepareDrm()在后台执行请求(如果需要)和准备。当drm准备就绪时,将调用侦听器。您不应该对调用序列或侦听器运行的线程做任何假设(除非侦听器注册到handler thread)。侦听器在prepareDrm()返回之前或之后能被调用。

您可以异步地初始化DRM,通过创建和注册MediaPlayer.OnDrmInfoListener用于DRM准备和MediaPlayer.OnDrmPreparedListener去启动播放器。它们与prepareAsync()协同工作,如下所示:

setOnPreparedListener();
setOnDrmInfoListener();
setDataSource();
prepareAsync();
// ...
​
// If the data source content is protected you receive a call to the onDrmInfo() callback.
onDrmInfo() {
  prepareDrm();
  getKeyRequest();
  provideKeyResponse();
}
​
// When prepareAsync() finishes, you receive a call to the onPrepared() callback.
// If there is a DRM, onDrmInfo() sets it up before executing this callback,
// so you can start the player.
onPrepared() {
​
start();
}

六、其他用法

1、处理加密媒体

从Android 8.0 (API级别26)开始,MediaPlayer还可以为H.264和AAC的基本流类型解密通用加密方案(CENC)和HLS采样级加密媒体(METHOD=SAMPLE-AES)。以前支持全段加密媒体(METHOD=AES-128)。

2、从ContentResolver检索媒体

在媒体播放器应用程序中可能有用的另一个特性是检索本地音乐。你可以通过查询外部媒体的ContentResolver来实现:

ContentResolver contentResolver = getContentResolver();
Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
Cursor cursor = contentResolver.query(uri, null, null, null, null);
if (cursor == null) {
    // query failed, handle error.
} else if (!cursor.moveToFirst()) {
    // no media on the device
} else {
    int titleColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE);
    int idColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media._ID);
    do {
       long thisId = cursor.getLong(idColumn);
       String thisTitle = cursor.getString(titleColumn);
       // ...process entry...
    } while (cursor.moveToNext());
}

在MediaPlayer中使用

long id = /* retrieve it from somewhere */;
Uri contentUri = ContentUris.withAppendedId(
        android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
​
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setDataSource(getApplicationContext(), contentUri);
​
// ...prepare and start...

七、实例

public class AudioPlayer extends ListActivity {
    String[] permissions = new String[]{
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.INTERNET,
            Manifest.permission.ACCESS_NETWORK_STATE
    };
    // 声明一个集合,在后面的代码中用来存储拒绝用户授权的权限
    List<String> mPermissionList = new ArrayList<>();

    /* 几个操作按钮 */
    private ImageButton mFrontImageButton = null;
    private ImageButton mStopImageButton = null;
    private ImageButton mStartImageButton = null;
    private ImageButton mPauseImageButton = null;
    private ImageButton mNextImageButton = null;

    /* MediaPlayer对象 */
    public MediaPlayer mMediaPlayer = null;

    /* 播放列表 */
    private List<String> mMusicList = new ArrayList<String>();

    /* 当前播放歌曲的索引 */
    private int currentListItme = 0;

    /* 音乐的路径 */
    private static final String MUSIC_PATH = new String("/sdcard/kgmusic/download/");

    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_audio);

        mPermissionList.clear();
        for (int i = 0; i < permissions.length; i++) {
            if (ContextCompat.checkSelfPermission(AudioPlayer.this, permissions[i]) != PackageManager.PERMISSION_GRANTED) {
                mPermissionList.add(permissions[i]);
            }
        }
        /* 更新显示播放列表 */
        musicList();
        /* 构建MediaPlayer对象 */
        mMediaPlayer = new MediaPlayer();

        mFrontImageButton = (ImageButton) findViewById(R.id.LastImageButton);
        mStopImageButton = (ImageButton) findViewById(R.id.StopImageButton);
        mStartImageButton = (ImageButton) findViewById(R.id.StartImageButton);
        mPauseImageButton = (ImageButton) findViewById(R.id.PauseImageButton);
        mNextImageButton = (ImageButton) findViewById(R.id.NextImageButton);

        //停止按钮
        mStopImageButton.setOnClickListener(new ImageButton.OnClickListener() {
            @Override
            public void onClick(View v) {
                /* 是否正在播放 */
                if (mMediaPlayer.isPlaying()) {
                    //重置MediaPlayer到初始状态
                    mMediaPlayer.reset();
                }
            }
        });
        //开始按钮
        mStartImageButton.setOnClickListener(new ImageButton.OnClickListener() {
            @Override
            public void onClick(View v) {
                playMusic(MUSIC_PATH + mMusicList.get(currentListItme));
            }
        });
        //暂停
        mPauseImageButton.setOnClickListener(new ImageButton.OnClickListener() {
            public void onClick(View view) {
                if (mMediaPlayer.isPlaying()) {
                    /* 暂停 */
                    mMediaPlayer.pause();
                } else {
                    /* 开始播放 */
                    mMediaPlayer.start();
                }
            }
        });
        //下一首
        mNextImageButton.setOnClickListener(new ImageButton.OnClickListener() {
            @Override
            public void onClick(View arg0) {
                nextMusic();
            }
        });
        //上一首
        mFrontImageButton.setOnClickListener(new ImageButton.OnClickListener() {
            @Override
            public void onClick(View arg0) {
                FrontMusic();
            }
        });
    }
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
            this.finish();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }
    @Override
    /* 当我们点击列表时,播放被点击的音乐 */
    protected void onListItemClick(ListView l, View v, int position, long id) {
        currentListItme = position;
        playMusic(MUSIC_PATH + mMusicList.get(position));
    }
    /* 播放列表 */
    public void musicList() {

        //取得指定位置的文件设置显示到播放列表
        File home = new File(MUSIC_PATH);

        if (!mPermissionList.isEmpty()) {//未授予的权限为空,表示都授予了
            String[] permissions = mPermissionList.toArray(new String[mPermissionList.size()]);//将List转为数组
            ActivityCompat.requestPermissions(AudioPlayer.this, permissions, 1);
        }
        //home.exists();
        try {
            if (home.listFiles(new MusicFilter()).length > 0) {
                for (File file : home.listFiles(new MusicFilter())) {
                    mMusicList.add(file.getName());
                }
                ArrayAdapter<String> musicList = new ArrayAdapter<String>(AudioPlayer.this, R.layout.musicitme, mMusicList);
                setListAdapter(musicList);
            }
        } catch (Exception e) {
            Log.i("TAG", e.toString(), null);
        }
    }
    private void playMusic(String path) {
        try {
            /* 重置MediaPlayer */
            mMediaPlayer.reset();
            /* 设置要播放的文件的路径 */
            mMediaPlayer.setDataSource(path);
            /* 准备播放 */
            mMediaPlayer.prepare();
            /* 开始播放 */
            mMediaPlayer.start();
            mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                public void onCompletion(MediaPlayer arg0) {
                    //播放完成一首之后进行下一首
                    nextMusic();
                }
            });
        } catch (IOException e) {
        }
    }
    /* 下一首 */
    private void nextMusic() {
        if (++currentListItme >= mMusicList.size()) {
            currentListItme = 0;
        } else {
            playMusic(MUSIC_PATH + mMusicList.get(currentListItme));
        }
    }
    /* 上一首 */
    private void FrontMusic() {
        if (--currentListItme >= 0) {
            currentListItme = mMusicList.size();
        } else {
            playMusic(MUSIC_PATH + mMusicList.get(currentListItme));
        }
    }
}
/* 过滤文件类型 */
class MusicFilter implements FilenameFilter {
    public boolean accept(File dir, String name) {
        //这里还可以设置其他格式的音乐文件
        return (name.endsWith(".mp3"));
    }
}