美文网首页Marked ArticlesAndroid进阶之旅Run_实用
Android开源音乐播放器之播放器基本功能

Android开源音乐播放器之播放器基本功能

作者: 王晨彦 | 来源:发表于2016-06-08 16:31 被阅读20328次

系列文章

前言

音乐播放器是我们最常用的应用之一,也是每部手机都会预装的应用。作为一个合格的音乐播放器,应该具有哪些功能呢?“无非是播放、暂停、切换歌曲、进度调节、切换播放模式、专辑封面显示、歌词显示、歌曲列表、歌曲管理(由于国产手机大多都是修改过的Android系统,因此系统自带播放器功能也不一样,这里以Android原生播放器为参考)这些功能” 一开始我也是这么认为的,但当我着手做的时候,才发现这些功能远远不够。如手机来电时,音乐需要自动暂停播放,耳机拔出时,同样需要暂停,还要支持耳机线控,等等,这些都是需要我们考虑的。

一个合格的音乐播放器应该具有哪些基本素质?

由于播放、暂停、切换歌曲、进度调节等这些功能过于简单,因此不过多讨论,这里只讨论一些容易被忽略的功能。

扫描本地音乐

扫描歌曲是播放器的基本功能,一般通过ContentProvider配合Media相关类查询系统数据库,获得媒体库中的歌曲信息。

/**
 * 扫描歌曲
 */
public static void scanMusic(Context context, List<Music> musicList) {
    musicList.clear();
    Cursor cursor = context.getContentResolver().query(
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            new String[]{
                    BaseColumns._ID,
                    MediaStore.Audio.AudioColumns.IS_MUSIC,
                    MediaStore.Audio.AudioColumns.TITLE,
                    MediaStore.Audio.AudioColumns.ARTIST,
                    MediaStore.Audio.AudioColumns.ALBUM,
                    MediaStore.Audio.AudioColumns.ALBUM_ID,
                    MediaStore.Audio.AudioColumns.DATA,
                    MediaStore.Audio.AudioColumns.DISPLAY_NAME,
                    MediaStore.Audio.AudioColumns.SIZE,
                    MediaStore.Audio.AudioColumns.DURATION
            },
            null,
            null,
            MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
    if (cursor == null) {
        return;
    }
    while (cursor.moveToNext()) {
        // 是否为音乐
        int isMusic = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.IS_MUSIC));
        if (isMusic == 0) {
            continue;
        }
        long id = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
        // 标题
        String title = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.TITLE)));
        // 艺术家
        String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ARTIST));
        // 专辑
        String album = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM)));
        // 专辑封面id,根据该id可以获得专辑封面图片
        long albumId = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM_ID));
        // 持续时间
        long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
        // 音乐文件路径
        String path = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DATA));
        // 音乐文件名
        String fileName = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DISPLAY_NAME)));
        // 音乐文件大小
        long fileSize = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.SIZE));
        Music music = new Music();
        music.set...
        musicList.add(music);
    }
    cursor.close();
}

/**
 * 从媒体库加载封面
 */
private Bitmap loadCoverFromMediaStore(long albumId) {
    ContentResolver resolver = mContext.getContentResolver();
    Uri uri = MusicUtils.getMediaStoreAlbumCoverUri(albumId);
    InputStream is;
    try {
        is = resolver.openInputStream(uri);
    } catch (FileNotFoundException ignored) {
        return null;
    }
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    return BitmapFactory.decodeStream(is, null, options);
}

通过以上方法基本可以获得音乐的所有信息,弊端是依赖于Android系统媒体库,有时新增音乐后没有通知系统扫描,就无法获得该音乐的信息,不够灵活。

避免播放器内存被系统回收

我们都知道Android系统有自动回收内存机制,如果系统内存紧张,就会触发该机制,应用就有可能被回收,不过Android提供了前台机制,保证内存不足时也不会回收该应用。

/**
 * 播放时启动前台机制
 */
public static void showPlay(Music music) {
    playService.startForeground(NOTIFICATION_ID, buildNotification(playService, music, true));
}

/**
 * 暂停时取消前台机制
 */
public static void showPause(Music music) {
    playService.stopForeground(false);
    notificationManager.notify(NOTIFICATION_ID, buildNotification(playService, music, false));
}

捕获/丢弃音乐焦点

大家可能不懂这个标题是什么意思,别着急,让我细细道来。
大家有没有试过,如果手机上安装了两个音乐播放器,当一个正在播放的时候,打开第二个播放歌曲,有没有发现第一个自动暂停了?
或者正在听歌时来电话了,音乐暂停了,挂断电话后音乐又继续播放了,
或者收到通知的时候音乐的音量变小了一下又恢复。

“-纳尼!难道不是自动暂停?”
“-图样图森破!”

这其实是因为播放器在后台处理了音频焦点的原因。

public class AudioFocusManager implements AudioManager.OnAudioFocusChangeListener {
    private PlayService mPlayService;
    private AudioManager mAudioManager;
    private boolean isPausedByFocusLossTransient;
    private int mVolumeWhenFocusLossTransientCanDuck;

    public AudioFocusManager(@NonNull PlayService playService) {
        mPlayService = playService;
        mAudioManager = (AudioManager) playService.getSystemService(AUDIO_SERVICE);
    }

    /**
     * 播放音乐前先请求音频焦点
     */
    public boolean requestAudioFocus() {
        return mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
                == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
    }

    /**
     * 退出播放器后不再占用音频焦点
     */
    public void abandonAudioFocus() {
        mAudioManager.abandonAudioFocus(this);
    }

    /**
     * 音频焦点监听回调
     */
    @Override
    public void onAudioFocusChange(int focusChange) {
        int volume;
        switch (focusChange) {
            // 重新获得焦点
            case AudioManager.AUDIOFOCUS_GAIN:
                if (!willPlay() && isPausedByFocusLossTransient) {
                    // 通话结束,恢复播放
                    mPlayService.playPause();
                }

                volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
                if (mVolumeWhenFocusLossTransientCanDuck > 0 && volume == mVolumeWhenFocusLossTransientCanDuck / 2) {
                    // 恢复音量
                    mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeWhenFocusLossTransientCanDuck,
                            AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
                }

                isPausedByFocusLossTransient = false;
                mVolumeWhenFocusLossTransientCanDuck = 0;
                break;
            // 永久丢失焦点,如被其他播放器抢占
            case AudioManager.AUDIOFOCUS_LOSS:
                if (willPlay()) {
                    forceStop();
                }
                break;
            // 短暂丢失焦点,如来电
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                if (willPlay()) {
                    forceStop();
                    isPausedByFocusLossTransient = true;
                }
                break;
            // 瞬间丢失焦点,如通知
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                // 音量减小为一半
                volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
                if (willPlay() && volume > 0) {
                    mVolumeWhenFocusLossTransientCanDuck = volume;
                    mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeWhenFocusLossTransientCanDuck / 2,
                            AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
                }
                break;
        }
    }

    private boolean willPlay() {
        return mPlayService.isPreparing() || mPlayService.isPlaying();
    }
}

耳机拔出时暂停播放

“-纳尼!难道耳机拔出时不是自动暂停吗?”
“-图样……”

private IntentFilter mNoisyFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);

public class NoisyAudioStreamReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 耳机拔出时暂停播放
        PlayService.startCommand(context, Actions.ACTION_MEDIA_PLAY_PAUSE);
    }
}

播放时注册广播接收器,暂停时取消注册即可。

联动系统媒体中心

这个标题大家可能也不懂,先放张图吧


明白了吧,我的播放器除了播放了一首音乐之外什么都没做,就可以分别在任务管理、锁屏、负一屏控制我的播放器了,是不是感觉碉堡了。
这些图是在我的小米手机上截的,不保证所有手机都有这些控制功能,但是只要你的Android版本是5.0以上,应该都会有媒体中心,无非是表现形式不同。
Android 5.0中新增了MediaSession类,官方说明是

允许与媒体控制器、音量键、媒体按钮和传输控件交互。

一个类包含了媒体控制和线控等功能,是不是很好用。
现在support-v4包加入了MediaSessionCompat用于在低版本上也能使用这个高大上的功能,
但是低版本上并不能实现媒体控制和线控等功能,低版本的线控功能我会在后面讲。

public class MediaSessionManager {
    private static final String TAG = "MediaSessionManager";
    private static final long MEDIA_SESSION_ACTIONS = PlaybackStateCompat.ACTION_PLAY
            | PlaybackStateCompat.ACTION_PAUSE
            | PlaybackStateCompat.ACTION_PLAY_PAUSE
            | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
            | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
            | PlaybackStateCompat.ACTION_STOP
            | PlaybackStateCompat.ACTION_SEEK_TO;

    private PlayService mPlayService;
    private MediaSessionCompat mMediaSession;

    public MediaSessionManager(PlayService playService) {
        mPlayService = playService;
        setupMediaSession();
    }

    /**
     * 初始化并激活MediaSession
     */
    private void setupMediaSession() {
        mMediaSession = new MediaSessionCompat(mPlayService, TAG);
        mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
                | MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS);
        mMediaSession.setCallback(callback);
        mMediaSession.setActive(true);
    }

    /**
     * 更新播放状态,播放/暂停/拖动进度条时调用
     */
    public void updatePlaybackState() {
        int state = (mPlayService.isPlaying() || mPlayService.isPreparing())
                ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
        mMediaSession.setPlaybackState(
                new PlaybackStateCompat.Builder()
                        .setActions(MEDIA_SESSION_ACTIONS)
                        .setState(state, mPlayService.getCurrentPosition(), 1)
                        .build());
    }

    /**
     * 更新正在播放的音乐信息,切换歌曲时调用
     */
    public void updateMetaData(Music music) {
        if (music == null) {
            mMediaSession.setMetadata(null);
            return;
        }

        MediaMetadataCompat.Builder metaData = new MediaMetadataCompat.Builder()
                .putString(MediaMetadataCompat.METADATA_KEY_TITLE, music.getTitle())
                .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, music.getArtist())
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, music.getAlbum())
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, music.getArtist())
                .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, music.getDuration())
                .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, CoverLoader.getInstance().loadThumbnail(music));

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            metaData.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, AppCache.getMusicList().size());
        }

        mMediaSession.setMetadata(metaData.build());
    }

    /**
     * 释放MediaSession,退出播放器时调用
     */
    public void release() {
        mMediaSession.setCallback(null);
        mMediaSession.setActive(false);
        mMediaSession.release();
    }

    private MediaSessionCompat.Callback callback = new MediaSessionCompat.Callback() {
        @Override
        public void onPlay() {
            mPlayService.playPause();
        }

        @Override
        public void onPause() {
            mPlayService.playPause();
        }

        @Override
        public void onSkipToNext() {
            mPlayService.next();
        }

        @Override
        public void onSkipToPrevious() {
            mPlayService.prev();
        }

        @Override
        public void onStop() {
            mPlayService.stop();
        }

        @Override
        public void onSeekTo(long pos) {
            mPlayService.seekTo((int) pos);
        }
    };
}

耳机线控(适用于API 19及以下)

“-纳尼……”
“-Shut up !”

是的,需要我们自己控制。
如果你已经按照上面的方法激活了MediaSession,那么在5.0以上的系统你已经不需要关心线控功能了,但是在5.0以上仍然需要自己监听耳机按键。

public class RemoteControlReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
        if (event == null || event.getAction() != KeyEvent.ACTION_UP) {
            return;
        }
        Intent serviceIntent;
        switch (event.getKeyCode()) {
            case KeyEvent.KEYCODE_MEDIA_PLAY:
            case KeyEvent.KEYCODE_MEDIA_PAUSE:
            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
            case KeyEvent.KEYCODE_HEADSETHOOK:
                serviceIntent = new Intent(context, PlayService.class);
                serviceIntent.setAction(Actions.ACTION_MEDIA_PLAY_PAUSE);
                context.startService(serviceIntent);
                break;
            case KeyEvent.KEYCODE_MEDIA_NEXT:
                serviceIntent = new Intent(context, PlayService.class);
                serviceIntent.setAction(Actions.ACTION_MEDIA_NEXT);
                context.startService(serviceIntent);
                break;
            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
                serviceIntent = new Intent(context, PlayService.class);
                serviceIntent.setAction(Actions.ACTION_MEDIA_PREVIOUS);
                context.startService(serviceIntent);
                break;
        }
    }
}

<!--在AndroidManifest中注册Receiver-->
<receiver android:name=".receiver.RemoteControlReceiver">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_BUTTON" />
    </intent-filter>
</receiver>

总结

感谢你能耐心的看到最后,本文主要讨论了音乐播放器容易忽略的重要功能,如果还有其他的本文没有提到的,请大家不吝赐教。

相关文章

网友评论

  • 星宇V:您好,我想问下,锁屏页的音频控制是再哪里写的?我加入action后还是不管用呀,总感觉少了什么东西
  • cdd8a84c52aa:这个退出之后是再次进去是怎么保证播放数据的,有没联系方式
    cdd8a84c52aa:@ChayWong 你那自动切换时候见过onCompletion被调用两次的没
    王晨彦:@御雨心空 利用service
  • 459e86d92be8:楼主能指点下底部的控制栏是如何实现的么,我使用BaseActivity在界面上能实现,但是在UI更新上就不行了,还望赐教,在下不胜感激
  • 麦香菌:求网易云音乐的接口
  • 趔趄徒成玦:请问MediaSessionManager这个类怎么使用
  • fylala:请问加速播放应该怎么做
  • 3a8525221520:我导入您的项目,报错,我是菜鸟,请问SDK版本号是多少?是不是Android studio报错,显示<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android";这里从http开始标红,
  • 51db34ac8ae9:为什么你的代码有时候会服务自己死掉,然后应用重启,怎么解决啊
  • 23da53754e07:你好,我按你的方式写了一个播放器,但是我手机开启锁屏清理后,播放器内存会被系统回收,但是你的app放到我手机里却不会?请问你是如何保活进程的?我也是播放时启用了startForeground前台机制啊。而且弹出了notification。不知道为什么却没有用。方便加个Q解答一下吗?1179290292,谢谢!
  • 2028fe579220:你好,请问下,播放歌曲锁屏之后,再亮屏时如何实现更改锁屏背景为专辑图片?就是类似于更换了锁屏壁纸那样的,不是把activity置于锁屏之上的那种
  • 猫不会微笑:请问有使用到aidl吗?
    王晨彦: @猫不会微笑 没有
  • DylanW:播放歌曲后,圆形图片好像有点模糊,是我这边的问题吗?
    王晨彦: @DylanW 云音乐应该是去自己服务器下载了高清封面。
    DylanW:@ChayWong 通过波尼音乐下载网络歌曲到本地,波尼音乐的封面是模糊的,网易云音乐的封面会从模糊变到清晰
    王晨彦: @DylanW 模糊应该是封面图片分辨率的问题。
  • DylanW:现在还不支持自动下一首是吗,点击按钮也不会调到下一首?谢谢
    王晨彦: @DylanW 你好,在线音乐暂时还不支持下一首
  • 29bd41aef701:请问下音乐在播放的状态下然后系统清理所有的应用音乐还在继续播放,这个是怎么实现的,仅仅就开启了一个前台进程么,我做的demo利用Service开启一个前台进程然后后台运行一段时间就自己死了,而且在开启很多个App的情况下也很容易死掉,请问下这个有没有好点的保活的方法
    23da53754e07:同问!我的手机设置了锁屏清理,但是这个波尼音乐却能常驻不被杀死。很想知道这点怎么实现的
  • 0青衣小褂0:放下电话之后继续播放需要怎么做呀
    王晨彦: @0青衣小褂0 你好,最近更新了文章,电话挂断后继续播放已经实现了。
  • 咖啡老师:写的挺好的,自己写图片加载缓存、注解等,很好的学习学习原理,很棒,感谢。
    王晨彦:@律动清风 过奖了,都是些很简单的东西~
  • George吴逸云:最近在看你的这个播放器,写的很好,学习了
  • Turaiiao:
    楼主加个好友?
    扣扣1171840237
  • 5e30a667061c:求源码!
    王晨彦:@流浪剑客矮人火枪手 http://www.jianshu.com/p/1c0f5c4f64fa
  • nbpzjy:好厉害学习了
  • 87efc67b3da6:请问我自己想办法取到了新的专辑封面,我要怎么保存到数据库里面?我的微信erliangfan。希望能回复。比较着急
    王晨彦:@erliangfan 从网络获取的封面建议保存文件到本地,方便下次直接从本地读取。
  • ae12:好厉害,我运行了你的程序,功能很多,我在一步步山寨呢,指望这个程序找工作。谢谢啦,以后代码上看不懂,我会尽力Google ,还是看不懂,就问你OK?
    王晨彦:@Liqing_1938 过奖了,有问题可以在文章下面评论。 :smile:

本文标题:Android开源音乐播放器之播放器基本功能

本文链接:https://www.haomeiwen.com/subject/kjejdttx.html