音乐播放器备忘录

最近写了一个开源的音乐应用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框架介绍

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容