Android API Media Playback(MediaPlayer)中文版

本文翻译自MediaPlayer--Android Developer,仅供学习参考,转载请标明出处(英语水平有限,如有问题,大家多多担待)。

Media Playback

      安卓的多媒体框架提供了对多种普通多媒体类型播放的支持,因此你可以很容易地把音频、视频和图片集成到你的app里。你可以播放存在你app资源文件夹(raw resource)中的音频或者视频,或者存在文件系统中的独立文件,再或者是通过网络连接获取的,这些都是使用MediaPlayer APIs。

      这篇文档向你展示了为了获取好的性能和友好的用户体验,应该怎样编写一个和用户或者系统交互的多媒体播放app。

注意:你只可以在标准的输出设备上播放音频数据。目前有移动设备的扬声器或者蓝牙耳机。你不能在打电话的时候播放声音文件。

基础知识

      下边的类用来在Android 框架中播放声音和视频。

MediaPlayer

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

AudioManager

       这个类用来管理音频资源,还有将音频输出到设备上

Manifest 声明

      在开始使用MediaPlayer在你的app上进行开发之前,确保你的manifest有允许使用相关features的对应声明。

  • Internet Permission 如果你使用MediaPlayer播放基于网络的内容,你的app必须添加网络访问权限
    <uses-permission android:name="android.permission.INTERNET" />
  • Wake Lock Permission 如果你的app需要防止屏幕变暗或者CPU进入休眠状态,或着要使用MediaPlayer.setScreenOnWhilePlaying()或者MediaPlayer.setWakeMode()方法,你必须声明该权限
    <uses-permission android:name="android.permission.WAKE_LOCK" />
使用MediaPlayer
       MediaPlayer是安卓多媒体框架中最重要的构件之一。该类的一个对象能够以最少的步骤获取、解码和播放音频和视频。它支持多种不同的资源,例如:
  • 本地资源
  • 内部URL,比如你从Content Resolver中获得的
  • 外部URL(流)
      对于Android支持的多媒体格式列表,请参考 Android Supported Media Formats
          这里有一个播放本地原始资源(保存在你app的res/raw文件夹下)的例子:
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // 不需要调用prepare(),create()帮你做了
在这个例子中,“原始”资源是系统不需要用特殊方式解析的文件。然而,这个资源的内容不应该是原始音频,它应该是一种以支持的格式正常编码和格式化的文件。
      这里告诉你怎样播放从本地获取到的URI(例如,你从Content Resolver中获取的)
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();
注意:如果你是将一个在线流媒体文件传给该URL,那么这个文件应该能够连续下载.
警告:在使用setDataResource()时,你必须捕获或者传递IllegalArgumentException和IOException,因为你所引用的文件可能不存在。

异步准备
       原则上可以直接使用MediaPlayer。然而,很多东西必须正确的集成到一个典型的Android app中,记住这一点是很重要的。例如,prepare()的调用可能会花费很长时间,因为它可能包括获取并解码多媒体数据。任何可能花费很长时间的方法,你都不能在UI线程中调用。因为这些方法会在返回前导致UI挂起,这是一个非常差的用户体验并且将引起ANR错误。即使你希望快速加载资源,但你要记住,任何事情在UI响应上花费超过100ms(原文1/10秒)都会导致用户意可识到的停顿,这将给用户留下你的app反应很慢的印象。
       为了避免挂起UI线程,应该开辟一个子线程来准备MediaPlayer,然后在完成的时候通知主线程。然而,当然你可以自己写线程逻辑,不过android框架通过使用prepareAsync()提供了一个方便的方式来实现准备任务,这个现象十分普遍。这个方法在后台开始准备并立刻返回。当多媒体准备好时,通过setOnPreparedListener()设置的MediaPlayer.OnPreparedListener的onPrepared()方法将会被调用。

管理状态
       你要牢记Mediaplayer的另一个特点是:它是基于状态的。也就是说,当你写代码的时候要知道MediaPlayer有一个内部的状态,因为特定的操作仅仅对指定的状态有效。如果你在错误的状态执行一个操作,系统将会抛出一个异常或者其他意想不到的结果。
       MediaPlayer类的说明文档给了一个完整的状态图,指明了什么方式使MediaPlayer从一个状态转变到另一个状态。例如,当你创建一个新的MediaPlayer,它是在Idle状态。这时,你应该调用setDataSource()来初始化,使它进入Initialized状态。在这之后,你必须使用prepare()或者prepareAsync()方法来准备它。当MediaPlayer完成准备时,它将进入Prepared状态,这意味着你可以调用start()来播放这个多媒体文件。那时,正如图片描述的,你可以通过调用start(),pause()和seekTo()还有其他的方法使其在Started,Paused和PlaybackCompleted状态之间转移。当你调用stop()方法后,可以注意到直到再次调用prepare之前,你不能再调用start()。


       当你编写代码和一个MediaPlayer对象交互时,你要一直记着状态图,因为在错误的状态调用它的方法是引起bug的普遍原因。

释放MediaPlayer
       一个MediaPlayer可能消耗各种系统资源。因此,你应该额外注意,一定不要在不再需要MediaPlayer的时候继续持有一个MediaPlayer。当你完成使用的时候,应该总是调用release()方法确保分配给其的任何系统资源都会被正常的释放。例如,如果你正在使用MediaPlayer,你的activity接收到onStop()的调用,你必须释放掉该MediaPlayer,因为你的activity不再和用户交互,继续持有它毫无意义(除非你在后台播放,这将在下一节讨论)。当你的activity resumed或者restarted时,当然你需要重新创建MediaPlayer并且在重新播放之前再次prepare。
       这里告诉你怎么释放并且置空MediaPlayer:
mediaPlayer.release();
mediaPlayer = null;
举例来说,考虑到你可能在你的activity onStop()的时候忘记release,但在activity再次start的时候创建一个新的。正如你知道的,当用户改变屏幕方向(或者以其他的方式改变了设备配置),系统会默认地重启activity,因此你可能在用户转换屏幕方向时很快的消耗掉系统资源,因为每次转换屏幕,你都创建一个新的MediaPlayer而没有释放。
       你可能想知道如果继续后台播放甚至用户离开了你的activity时继续播放将会发生什么,就像内置音乐应用那样。这种情况下,你需要的是一个service控制的MediaPlayer。

使用Service
       如果你想在后台播放,也就是说你希望在用户和其他app交互时仍然继续播放,那么你需要开启一个service来控制该MediaPlayer。你需要小心地设置,因为用户和系统希望一个后台运行的app应该能和系统其他部分交互。如果你的app没有达到,用户可能会感觉到不友好的体验。
    异步运行
       首先,类似于Activity,Service所有的工作都是在一个单线程中完成的,实际上,如果你是从同一个app启动的Activity和Service,它们默认使用的是同一个线程(即主线程)。因此,services需要快速处理收到的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();
    }
}
处理异步错误
    在异步操作中,错误通常显示为一个错误码或者异常,但不管什么时候使用异步资源,你应该确保你的app合适地收到错误通知。在MediaPlayer的例子中,你可以在你的MediaPlayer实例中通过实现MediaPlayer.OnErrorListener接口并设置来实现:
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!
    }
}
使用唤醒锁(wake locks)
          当设计一个能后台播放多媒体的app时,设备有可能在service运行时进入休眠状态。因为安卓系统会尽量在设备休眠状态时节省电量,系统会尽量关掉手机没有必要的任何特性,包括CPU、WiFi。然后,如果你的service正常播放或者流化音乐,你希望阻止系统干扰你的播放。
   为了确保你的service能在那些情况下继续运行,你不得不使用“wake locks”。wake lock是一种通知系统的方式,它告诉系统你的app正在使用一种特性:即手机在idle状态仍然能保持可用。
注意:你应该尽量少地使用wake lock并且尽在真正需要的时候使用,因为它确实会缩短设备电池的寿命。
   为了确保你的MediaPla播放的时候CPU继续运行,在初始化MediaPlayer的时候调用setWakeMode()。一旦你调用了,MediaPlayer会在播放的时候持有指定的锁,并且在暂停或者停止的时候释放。
mMediaPlayer = new MediaPlayer();
// ... other initialization here ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
        然而,这个例子中声明的wake锁只保证CPU不会休眠。如果你是通过网络流媒体并且是使用wifi来播放,那么你可能需要同时持有一个WifiLock,这个锁你需要手动获得并且手动释放。
</pre><pre name="code" class="java">WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
    .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");

wifiLock.acquire();
    当你暂停或者停止的时候,或者你不再需要网络的时候,你需要释放该锁:
wifiLock.release();
作为一个前台服务运行
   服务经常用来执行后台任务,比如获取emails、异步数据,下载内容或者其他可能的情况。在这些情况下,用户没有主动意识到服务的执行,也可能不会注意到一些服务中断或者重启。
   但是考虑到服务播放音乐的情况。很明显,这个服务是用户可以意识到的并且中断会严重影响体验。另外,这也是一个用户希望在执行过程中与之交互的服务。在这种情况下,服务应该以一个“前台服务”的方式运行。一个前台服务在系统中有着更高的优先级——系统一般从不杀死该服务。当运行在前台时,服务也会提供一个状态条通知来确保用户可以意识到运行的服务并允许他们打开一个Activity与这个服务进行交互。
为了使你的服务成为前台服务,你必须创建一个Notification,然后在Service中调用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,上面的例子中,它会打开MainActivity。

    你应该仅当你的服务正在执行一些用户可以感觉到的事情时才持有“前台服务”。一旦不需要的时候,你应该调用stopForeground()释放:
stopForeground(true);
    更多信息,查看关于Service和Status Bar Notification的文档。
处理音频焦点
    即使在任何给定时间只能有一个Activity,安卓是一个多任务环境。这对使用音频的应用构成了极大的挑战,只有一个设备输出,但可能有多个媒体服务竞争。在安卓2.2之前,没有内置机制来处理这个问题,这导致了很差的用户体验。例如,当一个用户正在听音乐,然而另一个应用程序需要通知用户一些重要的消息,用户可能由于大的音乐没有听到通知。在2.2之后,平台提供了一种方式来协商音频的输出设备。这个机制叫做音频焦点。
    当你的app需要输出音频例如音乐或者通知,你应该一直请求音频焦点。一旦拥有了焦点,它就可以自由得使用声音输出,但他应该倾听焦点的变化。如果它失去了焦点它应该被通知,它应该立即关闭这个音频或者降低声音到一个安静的水平,并且当获得焦点后恢复到原来的播放。
    音频焦点本质上是协作的。也就是说,系统是期望(并且高度鼓励)app遵守音频焦点规则的,但是这不是系统强制的。如果一个app在失去焦点后想要播放吵闹的音乐,系统不能阻止。然而,用户很可能会有一个差的体验并且很可能卸载表现不友好的app。
    为了获取音频焦点,你必须调用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()方法在音频焦点改变时被调用。因此,你也应该实现这个接口。例如:
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_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只在Android 2.2(API 8)以及以后的版本中,因此,如果你想支持之前的Android版本,你应该以一种向后兼容的方式。
你可以通过反射调用音频焦点来达到向后兼容或者通过在一个单独的类里面实现所有的音频焦点特征,这里有一个例子:
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对象消耗系统大量资源,所以应该在不用的时候释放。
public class MyService extends Service {
   MediaPlayer mMediaPlayer;
   // ...

   @Override
   public void onDestroy() {
       if (mMediaPlayer != null) mMediaPlayer.release();
   }
}
处理AUDIO_BECOMING_NOISY intent
    很多写良好的音频播放app会在一个导致音频成为噪音的事件发生时自动停止(通过扬声器播放)。例如,这可能发生在一个用户通过耳机听音乐,忽然耳机断开连接。
    然而,这个表现不会自动发生,如果你没有实现这一特征,声音从扬声器播出,这将不是用户想要的。
<receiver android:name=".MusicIntentReceiver">
   <intent-filter>
      <action android:name="android.media.AUDIO_BECOMING_NOISY" />
   </intent-filter>
</receiver>
这里注册了MusicIntentReciver类作为一个该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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值