知识点iOS学习开源项目

音乐播放器备忘录

2017-02-19  本文已影响999人  cpacm

最近写了一个开源的音乐应用beats,其最重要的内容就是如何完成一个完整的音乐播放器。这个播放器要既能播放网络上的音乐也能播放本地设备上的歌曲,同时也要具备常规的功能。

beats.png

一款完整的音乐播放器要具备哪些功能呢?

  1. 能播放音乐,这是最基础的功能;
  2. 能上下首切换,能暂停/播放,能拖动播放条进度,这也是常见的控制功能;
  3. 能在手机后台播放,现在没有这个功能都不好意思叫播放器了;
  4. 能有播放列表,能够进行循环,单曲或随机播放;
  5. 能在通知栏上显示歌曲播放信息;
  6. 能够同步显示歌词;
  7. 能够支持线控,所谓线控是指耳机上的按键能够控制播放器,耳机的插拔实现播放器的播放暂停也算属于这个功能内;
  8. 能在播放时进行音乐锁屏,好吧,其实我很讨厌这个功能,因为有了这个功能后大部分手机解锁时都要滑两次屏,有点画蛇添足的意味。

MusicService:在 Android 中后台任务一般使用 Service 来实现,所以可以建立一个 MusicService 来后台播放音乐。

MediaPlayerManager:同时为了更好的管理 MediaPlayer 可以再创建 MediaPlayerManager 类来管理音乐的控制。

MusicPlaylist:播放列表,记录当前要播放歌曲的列表,以便切换歌曲的播放。

音乐播放器细节备忘录

MediaPlayer

一般使用 MediaPlayer 就可以实现多媒体的播放,同时 SetWakeMode 为 PowerManager.PARTIAL_WAKE_LOCK 保证能够在后台运行。

mediaPlayer = new MediaPlayer();

// Make sure the media player will acquire a wake-lock while
// playing. If we don't do that, the CPU might go to sleep while the
// song is playing, causing playback to stop.
mediaPlayer.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);

这里同时要处理一下 AudioFocus 的问题,在播放前去请求硬件资源,播放结束后释放硬件资源。

/**
 * Try to get the system audio focus.
 */
audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
                AudioManager.AUDIOFOCUS_GAIN);

/**
 * Give up the audio focus.
 */
audioManager.abandonAudioFocus(this);

为了防止 Service 在运行一段时间后自动结束,在播放歌曲时要将其设置为前台服务。

public void setAsForeground() {
    startForeground(MusicNotification.NOTIFICATION_ID, MusicNotification.getNotification());
}

public void removeForeground(boolean removeNotification) {
    stopForeground(removeNotification);
}

使用 MediaSession 来控制播放器

MediaSession 框架是 Google 推出专门解决媒体播放时界面和服务通讯问题。这个框架可以让我们不再使用广播来控制播放器,而且也能适配耳机,蓝牙等一些其它设备,实现线控的功能。

mState = new PlaybackStateCompat.Builder()
        .setActions(
                ACTION_PLAY |
                        ACTION_PAUSE |
                        ACTION_PLAY_PAUSE |
                        ACTION_SKIP_TO_NEXT |
                        ACTION_SKIP_TO_PREVIOUS |
                        ACTION_STOP |
                        ACTION_PLAY_FROM_MEDIA_ID |
                        ACTION_PLAY_FROM_SEARCH |
                        ACTION_SKIP_TO_QUEUE_ITEM |
                        ACTION_SEEK_TO)
        .setState(state, PLAYBACK_POSITION_UNKNOWN, 1.0f, SystemClock.elapsedRealtime())
        .build();
mediaSession.setPlaybackState(mState);

线控的实现,MediaSessionCallback 类继承 MediaSessionCompat.Callback,利用 MusicPlayerManager 来实现 onPlay(),onPause,onSkipToNext()等一系列方法。

/**
 * 线控
 * 使用 {@link MediaButtonReceiver} 来兼容 api21 之前的版本
 * 使用{@link MediaSessionCompat#setCallback}控制 api21 之后的版本
 */
private void setUpMediaSession() {
    ComponentName mbr = new ComponentName(getPackageName(), MediaButtonReceiver.class.getName());
    mediaSession = new MediaSessionCompat(this, "fd", mbr, null);
    /* set flags to handle media buttons */
    mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
            MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
    /* this is need after Lolipop */
    mediaSession.setCallback(new MediaSessionCallback());
    setState(STATE_NONE);
}

通知栏

使用 NotificationCompat 创建通知栏信息就足够了。音乐播放器的通知栏一般选择 MediaStyle 风格就能用了。

PendingIntent stopServiceIntent = PendingIntent.getBroadcast(musicService, REQ_CODE, new Intent(ACTION_STOP), PendingIntent.FLAG_CANCEL_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(musicService);
        builder.setStyle(
                new NotificationCompat.MediaStyle().setShowActionsInCompactView(0, 1, 2, 3, 4)
                        .setMediaSession(musicService.getMediaSession().getSessionToken()).setShowCancelButton(true).setCancelButtonIntent(stopServiceIntent))
                .setSmallIcon(R.drawable.music)
                .setCategory(CATEGORY_TRANSPORT)
                .setVisibility(VISIBILITY_PUBLIC)
                .setDeleteIntent(stopServiceIntent)
                .setWhen(System.currentTimeMillis())
                .setContentIntent(PendingIntent.getActivity(musicService, REQ_CODE,
                        new Intent(musicService, SongPlayerActivity.class), PendingIntent.FLAG_CANCEL_CURRENT))
                .setPriority(PRIORITY_MAX);
                
// 添加按键动作,包括播放/暂停按钮,上一首下一首按钮
builder.addAction(R.drawable.ic_play_skip_previous, musicService.getString(R.string.music_previous), PendingIntent.getBroadcast(musicService, REQ_CODE,
                new Intent(ACTION_PREV), PendingIntent.FLAG_CANCEL_CURRENT));

if (musicService.getState() == STATE_PLAYING) {
    builder.addAction(R.drawable.ic_play, musicService.getString(R.string.music_pause), PendingIntent.getBroadcast(musicService, REQ_CODE,
            new Intent(ACTION_PAUSE), PendingIntent.FLAG_CANCEL_CURRENT));
} else {
    builder.addAction(R.drawable.ic_pause, musicService.getString(R.string.music_play), PendingIntent.getBroadcast(musicService, REQ_CODE,
            new Intent(ACTION_PLAY), PendingIntent.FLAG_CANCEL_CURRENT));
}

builder.addAction(R.drawable.ic_play_skip_next, musicService.getString(R.string.music_next), PendingIntent.getBroadcast(musicService, REQ_CODE,
        new Intent(ACTION_NEXT), PendingIntent.FLAG_CANCEL_CURRENT));

当歌曲播放状态发生变化时比如上下首切换,暂停等,都要重新向通知栏发送消息以便实时更新通知栏的歌曲信息。

NotificationManagerCompat.from(musicService).notify(NOTIFICATION_ID, getNotification());

在一些手机上(特别是MIUI)无法设置通知栏的缩略图片,只能通过自定义View 设置给 NotificationCompat 来避免这个问题。

歌词显示

一般都是利用自己实现的 LrcView 与播放器的进度进行同步,歌词的显示和滚动的效果都是交给 LrcView 处理。
步骤:从文件读取 Lrc 歌词,然后根据[00:02.32]解析歌词时间,与 MediaPlayer 进行同步,根据播放时间显示相应的歌词,动画效果和文字的显示则是交给 LrcView 来实现。

/** 
 * 解析歌词时间 
 * 歌词内容格式如下: 
 * [00:02.32]陈奕迅 
 * [00:03.43]好久不见 
 * [00:05.22]歌词制作  王涛 
 * @param timeStr 
 * @return 
 */  
public int time2Str(String timeStr) {  
    timeStr = timeStr.replace(":", ".");  
    timeStr = timeStr.replace(".", "@");  
      
    String timeData[] = timeStr.split("@"); //将时间分隔成字符串数组  
      
    //分离出分、秒并转换为整型  
    int minute = Integer.parseInt(timeData[0]);  
    int second = Integer.parseInt(timeData[1]);  
    int millisecond = Integer.parseInt(timeData[2]);  
      
    //计算上一行与下一行的时间转换为毫秒数  
    int currentTime = (minute * 60 + second) * 1000 + millisecond * 10;  
    return currentTime;  
}

本地歌曲

本地的专辑和歌曲都可以使用 Context.getContentResolver() 来进行查找,甚至连专辑的封面,艺人等信息也可以找到(只要歌曲携带这些信息,没有携带信息的歌曲都会标记为 unknown)

// 查找本地的歌曲
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, new String[]{"_id", "title", "artist", "album", "duration", "track", "artist_id", "album_id", "_data"}, selectionStatement, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);

//查找本地的专辑
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, new String[]{"_id", "album", "artist", "artist_id", "numsongs", "minyear"}, selection, paramArrayOfString, null);

//查找本地的艺人
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, new String[]{"_id", "artist", "number_of_albums", "number_of_tracks"}, selection, paramArrayOfString, null);

/**
 * 获取album的封面照片
 */
private static Uri getAlbumArtUri(long paramInt) {
    return ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), paramInt);
}

网络上的歌曲下载到本地时要想及时的出现在本地歌库中,需要手动的去扫描系统的多媒体库。

/**
     * 媒体扫描,防止下载后在sdcard中获取不到歌曲的信息
     *
     * @param path
     */
    public static void mp3Scanner(String path) {
        MediaScannerConnection.scanFile(CoreApplication.getInstance().getApplicationContext(),
                new String[]{path}, null, null);
    }

总结

上面只是列出了一些比较重要的代码,整体的代码可以参考我下面放的 Github 地址,里面有着完整的播放器源码,希望能够帮助你理清如何实现音乐播放的思路。

我自己写的播放器源码
MoeMusic-基于萌否网站api的音乐管理软件


其他一些参考的播放器源码:
googlesample-android-UniversalMusicPlayer
Timber-Material Design Music Player
ListenerMusicPlayer-A Grace Material Design Music Player

关于 MediaSession 的说明
Android:MediaSession框架介绍

上一篇下一篇

猜你喜欢

热点阅读