开发者社区 > 博文 > Android播放媒体详解
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

Android播放媒体详解

  • 京东城市JUST团队
  • 2021-01-27
  • IP归属:未知
  • 27720浏览

Android多媒体框架包括支持播放各种常见的媒体类型,以便您可以轻松地将音频,视频和图像集成到您的应用程序中。 您可以从存储在应用程序资源(原始资源)中的媒体文件,从文件系统中的独立文件或通过网络连接到达的数据流(均使用MediaPlayer API)播放音频或视频。

本文档介绍如何编写与用户和系统交互的媒体播放应用程序,以获得良好的性能和愉快的用户体验。

注意:您只能将音频数据播放到标准输出设备。 目前,这是移动设备扬声器或蓝牙耳机。 在通话期间无法在通话音频中播放声音文件。

基础

以下类用于在Android框架中播放声音和视频:

MediaPlayer

  • 这个类是播放声音和视频的主要API。

AudioManager

  • 此类管理设备上的音频源和音频输出。

MaAndroid多媒体框架包括支持播放各种常见的媒体类型,以便您可以轻松地将音频,视频和图像集成到您的应用程序中。 您可以从存储在应用程序资源(原始资源)中的媒体文件,从文件系统中的独立文件或通过网络连接到达的数据流(均使用MediaPlayer API)播放音频或视频。

本文档介绍如何编写与用户和系统交互的媒体播放应用程序,以获得良好的性能和愉快的用户体验。

注意:您只能将音频数据播放到标准输出设备。 目前,这是移动设备扬声器或蓝牙耳机。 在通话期间无法在通话音频中播放声音文件。

基础

以下类用于在Android框架中播放声音和视频:

MediaPlayer

  • 这个类是播放声音和视频的主要API。

AudioManager

  • 此类管理设备上的音频源和音频输出。

Manifest.xml

在使用MediaPlayer开始开发应用程序之前,请确保您的清单具有适当的声明,以允许使用相关功能。

Internet权限 - 如果您正在使用MediaPlayer来流式传输基于网络的内容,您的应用程序必须请求网络访问。

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

Wake Lock权限 - 如果您的播放器应用程序需要保持屏幕变暗或处理器不休眠,或使用MediaPlayer.setScreenOnWhilePlaying()或MediaPlayer.setWakeMode()方法,您必须请求此权限。

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

使用MediaPlayer

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

  • 本地资源
  • 内部URI,例如您可能从内容解析器获取的URI
  • 外部网址(流式传输)

有关Android支持的媒体格式列表,请参阅Android支持的媒体格式文档

下面是一个如何播放可用作本地原始资源(保存在应用程序的res / raw /目录中)的音频的示例:

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

在这种情况下,“原始”资源是系统不尝试以任何特定方式解析的文件。但是,此资源的内容不应是原始音频。它应该是以支持的格式之一正确编码和格式化的媒体文件。

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

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

通过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();

注意:如果您传递的网址流式传输在线媒体文件,该文件必须能够逐步下载。

警告:当使用setDataSource()时,必须捕获或传递IllegalArgumentException和IOException,因为您引用的文件可能不存在。

异步准备

使用MediaPlayer原则上可以是直接的。然而,重要的是要记住,需要几个东西来正确地集成它与一个典型的Android应用程序。例如,prepare()的调用可能需要很长时间才能执行,因为它可能涉及提取和解码媒体数据。所以,就像任何可能需要很长时间来执行的方法,你不应该从应用程序的UI线程调用它。这样做会导致UI挂起,直到方法返回,这是一个非常糟糕的用户体验,并可能导致ANR(应用程序不响应)错误。如果您希望资源快速加载,请记住,在UI中响应超过十分之一秒的任何内容都会导致明显的暂停,并给用户留下您的应用程序运行缓慢的印象。

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

管理状态

MediaPlayer的另一个方面你应该记住它是基于状态的。也就是说,MediaPlayer有一个内部状态,你在编写代码时必须始终注意,因为某些操作只有在播放器处于特定状态时才有效。如果在错误状态下执行操作,系统可能会抛出异常或导致其他不需要的行为。

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

当编写与MediaPlayer对象交互的代码时,始终保持状态图,因为从错误状态调用其方法是错误的常见原因。

释放MediaPlayer

MediaPlayer可以消耗有价值的系统资源。因此,您应该总是采取额外的预防措施,以确保您没有挂在MediaPlayer实例更长的时间。当你完成它,你应该总是调用release(),以确保分配给它的任何系统资源正确释放。例如,如果您使用的是MediaPlayer,并且您的Activity接收到对onStop()的调用,则必须释放MediaPlayer,因为在Activity没有与用户交互的情况下保持它是没有意义的(除非您正在播放媒体在后台,这将在下面讨论)。当您的Activity恢复或重新启动时,当然,您需要创建一个新的MediaPlayer,并在恢复播放之前再次准备。

这里是如何释放,然后nullize你的MediaPlayer:

mediaPlayer.release();
mediaPlayer = null;

例如,考虑如果您在Activity停止时忘记释放MediaPlayer,但在Activity再次启动时创建一个新的MediaPlayer,可能会发生的问题。您可能知道,当用户更改屏幕方向(或以其他方式更改设备配置)时,系统会通过重新启动Activity(默认情况下)来处理,因此您可能会在用户旋转时快速消耗所有系统资源设备在纵向和横向之间来回移动,因为在每个方向更改时,您将创建一个不会释放的新MediaPlayer。

你可能想知道,如果你想继续播放“后台媒体”,即使用户离开你的Activity,很大程度上与内置音乐应用程序的行为相同的方式会发生什么。在这种情况下,您需要的是由服务控制的MediaPlayer,如在下面所述。

使用服务与MediaPlayer

如果您希望您的媒体在后台播放,即使您的应用程序不在屏幕上,也就是说,您希望它在用户与其他应用程序交互时继续播放 - 那么您必须启动一个服务并从那里控制MediaPlayer实例。您应该小心这个设置,因为用户和系统都期望运行后台服务的应用程序如何与系统的其余部分交互。如果您的应用程序不满足这些期望,用户可能会有不好的体验。本节介绍了您应该注意的主要问题,并提供有关如何处理这些问题的建议。

异步运行

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

例如,当从主线程使用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();
    }
}

处理异步错误

在同步操作中,通常会使用异常或错误代码来通知错误,但是每当使用异步资源时,都应该确保适当地通知应用程序错误。在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移动到错误状态(请参阅MediaPlayer类的完整状态图),您必须重置它,然后才能再次使用它。

使用Wake Lock(唤醒锁)

当设计在后台播放媒体的应用程序时,设备可能会在服务运行时进入睡眠状态。由于Android系统尝试在设备休眠时节省电池,因此系统会尝试关闭手机的任何不需要的功能,包括CPU和WiFi硬件。但是,如果您的服务正在播放或流式传输音乐,则希望防止系统干扰您的播放。

为了确保您的服务在这些条件下继续运行,您必须使用“唤醒锁”。唤醒锁是通知系统您的应用程序正在使用一些应该保持可用即使电话空闲的功能的一种方式。

注意:您应始终谨慎使用唤醒锁,只在真正必要的时间内保持它们,因为它们会显着降低设备的电池寿命。

要确保CPU在播放MediaPlayer时继续运行,请在初始化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();

作为前台服务运行

服务通常用于执行后台任务,诸如获取电子邮件,同步数据,下载内容以及其他可能。在这些情况下,用户没有主动地了解服务的执行,并且可能甚至不会注意到这些服务中的一些是否被中断并且稍后重新启动。

但考虑一种正在播放音乐的服务的情况。显然,这是用户主动了解的服务,并且经历将受到任何中断的严重影响。此外,它是用户在执行期间可能希望与之交互的服务。在这种情况下,服务应作为“前台服务”运行。前台服务在系统中具有更高的重要性 - 系统几乎不会杀死服务,因为它对用户是直接重要的。当在前台运行时,服务还必须提供状态栏通知,以确保用户知道正在运行的服务并允许他们打开可与服务交互的Activity。

为了将您的服务转换为前台服务,您必须为状态栏创建通知并从服务调用startForeground()。例如:

String songName;
// assign the song name to songName
PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,
                new Intent(getApplicationContext(), MainActivity.class),
                PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification();
notification.tickerText = text;
notification.icon = R.drawable.play0;
notification.flags |= Notification.FLAG_ONGOING_EVENT;
notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample",
                "Playing: " + songName, pi);
startForeground(NOTIFICATION_ID, notification);

当您的服务在前台运行时,您配置的通知在设备的通知区域中可见。如果用户选择通知,系统调用您提供的PendingIntent。在上面的示例中,它打开一个Activity(MainActivity)。

您应该只保持“前台服务”状态,而您的服务实际执行用户主动感知的东西。 一旦不这样,你应该通过调用stopForeground()释放它:

stopForeground(true);

处理音频焦点

即使只有一个Activity可以在任何给定的时间运行,Android仍是一个多任务环境。这对使用音频的应用提出了特别的挑战,因为只有一个音频输出,并且可能存在若干媒体服务竞争其使用。在Android 2.2之前,没有内置的机制来解决这个问题,这在某些情况下可能会导致糟糕的用户体验。例如,当用户正在听音乐并且另一应用需要向用户通知非常重要的事情时,由于大声的音乐,用户可能听不到通知铃声。从Android 2.2开始,平台为应用程序提供了一种方式,协商其使用设备的音频输出。这种机制称为音频焦点。

当您的应用程序需要输出音频(如音乐或通知)时,应始终请求音频焦点。一旦它有焦点,它可以自由地使用声音输出,但它应该总是监听焦点更改。如果通知它已经失去了音频焦点,它应该立即杀死音频或降低到一个安静的水平(称为“ducking(下降,回避)” - 有一个标志,指示哪一个是适当的),并且只有恢复高声播放后它再次接收焦点。

音频焦点在性质上是合作的。也就是说,应用程序需要(并高度鼓励)遵守音频焦点指南,但规则不是由系统强制执行。如果应用程序想要播放大声的音乐,即使失去音频焦点,系统中的任何东西都不会阻止。然而,用户更可能具有不良体验,并且将更可能卸载行为不当的应用。

要请求音频焦点,您必须从AudioManager调用requestAudioFocus(),如下面的示例所示:

AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
    AudioManager.AUDIOFOCUS_GAIN);

if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // could not get audio focus.
}

requestAudioFocus()的第一个参数是AudioManager.OnAudioFocusChangeListener,只要音频焦点发生变化,就会调用onAudioFocusChange()方法。因此,您还应该对您的服务和Activity实现此接口。例如:

class MyService extends Service
                implements AudioManager.OnAudioFocusChangeListener {
    // ....
    public void onAudioFocusChange(int focusChange) {
        // Do something based on focus change...
    }
}

focusChange参数告诉您音频焦点是如何改变的,并且可以是以下值之一(它们都是在AudioManager中定义的常量):

  • AUDIOFOCUS_GAIN:你已经获得了音频焦点。
  • AUDIOFOCUS_LOSS:你已经失去了音频焦点大概很长时间。您必须停止所有音频播放。因为你应该期望没有焦点回来,这将是一个很好的地方,尽可能多地清理你的资源。例如,您应该释放MediaPlayer。
  • AUDIOFOCUS_LOSS_TRANSIENT:您暂时失去了音频焦点,但很快就会收到。您必须停止所有音频播放,但您可以保留您的资源,因为您很可能会很快回到焦点。
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:您暂时失去了音频焦点,但您可以继续静静地播放音频(音量低),而不是完全停止音频。

这里是一个示例实现:

public void onAudioFocusChange(int focusChange) {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_GAIN:
            // resume playback
            if (mMediaPlayer == null) initMediaPlayer();
            else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start();
            mMediaPlayer.setVolume(1.0f, 1.0f);
            break;

        case AudioManager.AUDIOFOCUS_LOSS:
            // Lost focus for an unbounded amount of time: stop playback and release media player
            if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
            break;

        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            // Lost focus for a short time, but we have to stop
            // playback. We don't release the media player because playback
            // is likely to resume
            if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();
            break;

        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // Lost focus for a short time, but it's ok to keep playing
            // at an attenuated level
            if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f);
            break;
    }
}

请注意,音频焦点API仅适用于API级别8(Android 2.2)及更高版本,因此如果您要支持以前的Android版本,则应采用向后兼容性策略,以便在使用此功能(如果有),并且不是无缝地回退。

您可以通过反射调用音频焦点方法或通过在单独的类(例如AudioFocusHelper)中实现所有音频焦点功能来实现向后兼容。这里有一个这样的类的例子:

public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener {
    AudioManager mAudioManager;

    // other fields here, you'll probably hold a reference to an interface
    // that you can use to communicate the focus changes to your Service

    public AudioFocusHelper(Context ctx, /* other arguments here */) {
        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        // ...
    }

    public boolean requestFocus() {
        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
            mAudioManager.requestAudioFocus(mContext, AudioManager.STREAM_MUSIC,
            AudioManager.AUDIOFOCUS_GAIN);
    }

    public boolean abandonFocus() {
        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
            mAudioManager.abandonAudioFocus(this);
    }

    @Override
    public void onAudioFocusChange(int focusChange) {
        // let your service know about the focus change
    }
}

仅当检测到系统运行的API级别为8或更高时,才能创建AudioFocusHelper类的实例。例如:

if (android.os.Build.VERSION.SDK_INT >= 8) {
    mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
} else {
    mAudioFocusHelper = null;
}

执行清理

如前所述,MediaPlayer对象可能消耗大量的系统资源,因此您应该只保留它,只要您需要,并在完成后调用release()。重要的是明确调用这个清除方法,而不是依赖系统垃圾收集,因为它可能需要一些时间,垃圾收集器回收MediaPlayer,它只对内存需求敏感,而不是缺乏其他媒体相关资源。因此,在使用服务的情况下,您应该总是覆盖onDestroy()方法,以确保您释放MediaPlayer:

public class MyService extends Service {
   MediaPlayer mMediaPlayer;
   // ...

   @Override
   public void onDestroy() {
       if (mMediaPlayer != null) mMediaPlayer.release();
   }
}

您应该总是寻找其他机会释放您的MediaPlayer,除了在关闭时释放它。例如,如果您预计不能长时间播放媒体(例如,失去音频焦点后),您应该肯定会释放您现有的MediaPlayer并稍后重新创建。另一方面,如果你只希望停止播放很短的时间,你应该保留你的MediaPlayer,以避免创建和准备再次的开销。

处理AUDIO_BECOMING_NOISY Intent

许多精心编写的播放音频的应用程序会在发生导致音频变得嘈杂(通过外部扬声器输出)的事件时自动停止播放。例如,当用户通过耳机收听音乐并意外地将耳机从设备断开时,可能发生这种情况。但是,此行为不会自动发生。如果您不实现此功能,音频会从设备的外部扬声器播放,这可能不是用户想要的。

您可以通过处理ACTION_AUDIO_BECOMING_NOISY Intent,确保您的应用在这些情况下停止播放音乐,您可以通过向清单添加以下内容来注册接收者:

<receiver android:name=".MusicIntentReceiver">
   <intent-filter>
      <action android:name="android.media.AUDIO_BECOMING_NOISY" />
   </intent-filter>
</receiver>

这将MusicIntentReceiver类注册为该Intent的广播接收器。然后你应该实现这个类:

public class MusicIntentReceiver extends android.content.BroadcastReceiver {
   @Override
   public void onReceive(Context ctx, Intent intent) {
      if (intent.getAction().equals(
                    android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
          // signal your service to stop playback
          // (via an Intent, for instance)
      }
   }
}

从内容解析器检索媒体

在媒体播放器应用中可能有用的另一个特征是检索用户在设备上具有的音乐的能力。您可以通过查询外部媒体的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...

在使用MediaPlayer开始开发应用程序之前,请确保您的清单具有适当的声明,以允许使用相关功能。

Internet权限 - 如果您正在使用MediaPlayer来流式传输基于网络的内容,您的应用程序必须请求网络访问。

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

Wake Lock权限 - 如果您的播放器应用程序需要保持屏幕变暗或处理器不休眠,或使用MediaPlayer.setScreenOnWhilePlaying()或MediaPlayer.setWakeMode()方法,您必须请求此权限。

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

使用MediaPlayer

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

  • 本地资源
  • 内部URI,例如您可能从内容解析器获取的URI
  • 外部网址(流式传输)

有关Android支持的媒体格式列表,请参阅Android支持的媒体格式文档

下面是一个如何播放可用作本地原始资源(保存在应用程序的res / raw /目录中)的音频的示例:

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

在这种情况下,“原始”资源是系统不尝试以任何特定方式解析的文件。但是,此资源的内容不应是原始音频。它应该是以支持的格式之一正确编码和格式化的媒体文件。

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

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

通过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();

注意:如果您传递的网址流式传输在线媒体文件,该文件必须能够逐步下载。

警告:当使用setDataSource()时,必须捕获或传递IllegalArgumentException和IOException,因为您引用的文件可能不存在。

异步准备

使用MediaPlayer原则上可以是直接的。然而,重要的是要记住,需要几个东西来正确地集成它与一个典型的Android应用程序。例如,prepare()的调用可能需要很长时间才能执行,因为它可能涉及提取和解码媒体数据。所以,就像任何可能需要很长时间来执行的方法,你不应该从应用程序的UI线程调用它。这样做会导致UI挂起,直到方法返回,这是一个非常糟糕的用户体验,并可能导致ANR(应用程序不响应)错误。如果您希望资源快速加载,请记住,在UI中响应超过十分之一秒的任何内容都会导致明显的暂停,并给用户留下您的应用程序运行缓慢的印象。

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

管理状态

MediaPlayer的另一个方面你应该记住它是基于状态的。也就是说,MediaPlayer有一个内部状态,你在编写代码时必须始终注意,因为某些操作只有在播放器处于特定状态时才有效。如果在错误状态下执行操作,系统可能会抛出异常或导致其他不需要的行为。

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

当编写与MediaPlayer对象交互的代码时,始终保持状态图,因为从错误状态调用其方法是错误的常见原因。

释放MediaPlayer

MediaPlayer可以消耗有价值的系统资源。因此,您应该总是采取额外的预防措施,以确保您没有挂在MediaPlayer实例更长的时间。当你完成它,你应该总是调用release(),以确保分配给它的任何系统资源正确释放。例如,如果您使用的是MediaPlayer,并且您的Activity接收到对onStop()的调用,则必须释放MediaPlayer,因为在Activity没有与用户交互的情况下保持它是没有意义的(除非您正在播放媒体在后台,这将在下面讨论)。当您的Activity恢复或重新启动时,当然,您需要创建一个新的MediaPlayer,并在恢复播放之前再次准备。

这里是如何释放,然后nullize你的MediaPlayer:

mediaPlayer.release();
mediaPlayer = null;

例如,考虑如果您在Activity停止时忘记释放MediaPlayer,但在Activity再次启动时创建一个新的MediaPlayer,可能会发生的问题。您可能知道,当用户更改屏幕方向(或以其他方式更改设备配置)时,系统会通过重新启动Activity(默认情况下)来处理,因此您可能会在用户旋转时快速消耗所有系统资源设备在纵向和横向之间来回移动,因为在每个方向更改时,您将创建一个不会释放的新MediaPlayer。

你可能想知道,如果你想继续播放“后台媒体”,即使用户离开你的Activity,很大程度上与内置音乐应用程序的行为相同的方式会发生什么。在这种情况下,您需要的是由服务控制的MediaPlayer,如在下面所述。

使用服务与MediaPlayer

如果您希望您的媒体在后台播放,即使您的应用程序不在屏幕上,也就是说,您希望它在用户与其他应用程序交互时继续播放 - 那么您必须启动一个服务并从那里控制MediaPlayer实例。您应该小心这个设置,因为用户和系统都期望运行后台服务的应用程序如何与系统的其余部分交互。如果您的应用程序不满足这些期望,用户可能会有不好的体验。本节介绍了您应该注意的主要问题,并提供有关如何处理这些问题的建议。

异步运行

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

例如,当从主线程使用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();
    }
}

处理异步错误

在同步操作中,通常会使用异常或错误代码来通知错误,但是每当使用异步资源时,都应该确保适当地通知应用程序错误。在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移动到错误状态(请参阅MediaPlayer类的完整状态图),您必须重置它,然后才能再次使用它。

使用Wake Lock(唤醒锁)

当设计在后台播放媒体的应用程序时,设备可能会在服务运行时进入睡眠状态。由于Android系统尝试在设备休眠时节省电池,因此系统会尝试关闭手机的任何不需要的功能,包括CPU和WiFi硬件。但是,如果您的服务正在播放或流式传输音乐,则希望防止系统干扰您的播放。

为了确保您的服务在这些条件下继续运行,您必须使用“唤醒锁”。唤醒锁是通知系统您的应用程序正在使用一些应该保持可用即使电话空闲的功能的一种方式。

注意:您应始终谨慎使用唤醒锁,只在真正必要的时间内保持它们,因为它们会显着降低设备的电池寿命。

要确保CPU在播放MediaPlayer时继续运行,请在初始化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();

作为前台服务运行

服务通常用于执行后台任务,诸如获取电子邮件,同步数据,下载内容以及其他可能。在这些情况下,用户没有主动地了解服务的执行,并且可能甚至不会注意到这些服务中的一些是否被中断并且稍后重新启动。

但考虑一种正在播放音乐的服务的情况。显然,这是用户主动了解的服务,并且经历将受到任何中断的严重影响。此外,它是用户在执行期间可能希望与之交互的服务。在这种情况下,服务应作为“前台服务”运行。前台服务在系统中具有更高的重要性 - 系统几乎不会杀死服务,因为它对用户是直接重要的。当在前台运行时,服务还必须提供状态栏通知,以确保用户知道正在运行的服务并允许他们打开可与服务交互的Activity。

为了将您的服务转换为前台服务,您必须为状态栏创建通知并从服务调用startForeground()。例如:

String songName;
// assign the song name to songName
PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,
                new Intent(getApplicationContext(), MainActivity.class),
                PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification();
notification.tickerText = text;
notification.icon = R.drawable.play0;
notification.flags |= Notification.FLAG_ONGOING_EVENT;
notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample",
                "Playing: " + songName, pi);
startForeground(NOTIFICATION_ID, notification);

当您的服务在前台运行时,您配置的通知在设备的通知区域中可见。如果用户选择通知,系统调用您提供的PendingIntent。在上面的示例中,它打开一个Activity(MainActivity)。

您应该只保持“前台服务”状态,而您的服务实际执行用户主动感知的东西。 一旦不这样,你应该通过调用stopForeground()释放它:

stopForeground(true);

处理音频焦点

即使只有一个Activity可以在任何给定的时间运行,Android仍是一个多任务环境。这对使用音频的应用提出了特别的挑战,因为只有一个音频输出,并且可能存在若干媒体服务竞争其使用。在Android 2.2之前,没有内置的机制来解决这个问题,这在某些情况下可能会导致糟糕的用户体验。例如,当用户正在听音乐并且另一应用需要向用户通知非常重要的事情时,由于大声的音乐,用户可能听不到通知铃声。从Android 2.2开始,平台为应用程序提供了一种方式,协商其使用设备的音频输出。这种机制称为音频焦点。

当您的应用程序需要输出音频(如音乐或通知)时,应始终请求音频焦点。一旦它有焦点,它可以自由地使用声音输出,但它应该总是监听焦点更改。如果通知它已经失去了音频焦点,它应该立即杀死音频或降低到一个安静的水平(称为“ducking(下降,回避)” - 有一个标志,指示哪一个是适当的),并且只有恢复高声播放后它再次接收焦点。

音频焦点在性质上是合作的。也就是说,应用程序需要(并高度鼓励)遵守音频焦点指南,但规则不是由系统强制执行。如果应用程序想要播放大声的音乐,即使失去音频焦点,系统中的任何东西都不会阻止。然而,用户更可能具有不良体验,并且将更可能卸载行为不当的应用。

要请求音频焦点,您必须从AudioManager调用requestAudioFocus(),如下面的示例所示:

AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
    AudioManager.AUDIOFOCUS_GAIN);

if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // could not get audio focus.
}

requestAudioFocus()的第一个参数是AudioManager.OnAudioFocusChangeListener,只要音频焦点发生变化,就会调用onAudioFocusChange()方法。因此,您还应该对您的服务和Activity实现此接口。例如:

class MyService extends Service
                implements AudioManager.OnAudioFocusChangeListener {
    // ....
    public void onAudioFocusChange(int focusChange) {
        // Do something based on focus change...
    }
}

focusChange参数告诉您音频焦点是如何改变的,并且可以是以下值之一(它们都是在AudioManager中定义的常量):

  • AUDIOFOCUS_GAIN:你已经获得了音频焦点。
  • AUDIOFOCUS_LOSS:你已经失去了音频焦点大概很长时间。您必须停止所有音频播放。因为你应该期望没有焦点回来,这将是一个很好的地方,尽可能多地清理你的资源。例如,您应该释放MediaPlayer。
  • AUDIOFOCUS_LOSS_TRANSIENT:您暂时失去了音频焦点,但很快就会收到。您必须停止所有音频播放,但您可以保留您的资源,因为您很可能会很快回到焦点。
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:您暂时失去了音频焦点,但您可以继续静静地播放音频(音量低),而不是完全停止音频。

这里是一个示例实现:

public void onAudioFocusChange(int focusChange) {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_GAIN:
            // resume playback
            if (mMediaPlayer == null) initMediaPlayer();
            else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start();
            mMediaPlayer.setVolume(1.0f, 1.0f);
            break;

        case AudioManager.AUDIOFOCUS_LOSS:
            // Lost focus for an unbounded amount of time: stop playback and release media player
            if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
            break;

        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            // Lost focus for a short time, but we have to stop
            // playback. We don't release the media player because playback
            // is likely to resume
            if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();
            break;

        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // Lost focus for a short time, but it's ok to keep playing
            // at an attenuated level
            if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f);
            break;
    }
}

请注意,音频焦点API仅适用于API级别8(Android 2.2)及更高版本,因此如果您要支持以前的Android版本,则应采用向后兼容性策略,以便在使用此功能(如果有),并且不是无缝地回退。

您可以通过反射调用音频焦点方法或通过在单独的类(例如AudioFocusHelper)中实现所有音频焦点功能来实现向后兼容。这里有一个这样的类的例子:

public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener {
    AudioManager mAudioManager;

    // other fields here, you'll probably hold a reference to an interface
    // that you can use to communicate the focus changes to your Service

    public AudioFocusHelper(Context ctx, /* other arguments here */) {
        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        // ...
    }

    public boolean requestFocus() {
        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
            mAudioManager.requestAudioFocus(mContext, AudioManager.STREAM_MUSIC,
            AudioManager.AUDIOFOCUS_GAIN);
    }

    public boolean abandonFocus() {
        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
            mAudioManager.abandonAudioFocus(this);
    }

    @Override
    public void onAudioFocusChange(int focusChange) {
        // let your service know about the focus change
    }
}

仅当检测到系统运行的API级别为8或更高时,才能创建AudioFocusHelper类的实例。例如:

if (android.os.Build.VERSION.SDK_INT >= 8) {
    mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
} else {
    mAudioFocusHelper = null;
}

执行清理

如前所述,MediaPlayer对象可能消耗大量的系统资源,因此您应该只保留它,只要您需要,并在完成后调用release()。重要的是明确调用这个清除方法,而不是依赖系统垃圾收集,因为它可能需要一些时间,垃圾收集器回收MediaPlayer,它只对内存需求敏感,而不是缺乏其他媒体相关资源。因此,在使用服务的情况下,您应该总是覆盖onDestroy()方法,以确保您释放MediaPlayer:

public class MyService extends Service {
   MediaPlayer mMediaPlayer;
   // ...

   @Override
   public void onDestroy() {
       if (mMediaPlayer != null) mMediaPlayer.release();
   }
}

您应该总是寻找其他机会释放您的MediaPlayer,除了在关闭时释放它。例如,如果您预计不能长时间播放媒体(例如,失去音频焦点后),您应该肯定会释放您现有的MediaPlayer并稍后重新创建。另一方面,如果你只希望停止播放很短的时间,你应该保留你的MediaPlayer,以避免创建和准备再次的开销。

处理AUDIO_BECOMING_NOISY Intent

许多精心编写的播放音频的应用程序会在发生导致音频变得嘈杂(通过外部扬声器输出)的事件时自动停止播放。例如,当用户通过耳机收听音乐并意外地将耳机从设备断开时,可能发生这种情况。但是,此行为不会自动发生。如果您不实现此功能,音频会从设备的外部扬声器播放,这可能不是用户想要的。

您可以通过处理ACTION_AUDIO_BECOMING_NOISY Intent,确保您的应用在这些情况下停止播放音乐,您可以通过向清单添加以下内容来注册接收者:

<receiver android:name=".MusicIntentReceiver">
   <intent-filter>
      <action android:name="android.media.AUDIO_BECOMING_NOISY" />
   </intent-filter>
</receiver>

这将MusicIntentReceiver类注册为该Intent的广播接收器。然后你应该实现这个类:

public class MusicIntentReceiver extends android.content.BroadcastReceiver {
   @Override
   public void onReceive(Context ctx, Intent intent) {
      if (intent.getAction().equals(
                    android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
          // signal your service to stop playback
          // (via an Intent, for instance)
      }
   }
}

从内容解析器检索媒体

在媒体播放器应用中可能有用的另一个特征是检索用户在设备上具有的音乐的能力。您可以通过查询外部媒体的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...
共0条评论