Skip to content

[ZH] 3.自定义 PlayerService

jrfeng edited this page Mar 7, 2021 · 22 revisions

PlayerService 类被设计成可扩展的,你可以通过覆盖它的对应方法来实现以下功能:

  1. 自定义音乐播放器
  2. 自定义通知栏控制器
  3. 自定义音频特效引擎
  4. 记录播放历史
  5. 音质/动态 URL
  6. 仅 Wifi 网络播放
  7. 线控播放
  8. 自定义 MediaSessionCompat.Callback

其他内容

注意!PlayerService 不是线程安全的,请不要在异步线程中调用它的方法,否则结果是未知的。

1. 自定义音乐播放器

如果默认的 MediaMusicPlayer 不能满足你的需求,则可以覆盖 onCreateMusicPlayer(Context, MusicItem, Uri) 方法并提供一个其他的 MusicPlayer 实现(例如:ExoMusicPlayer,参见:使用 ExoPlayer)。

例:

@PersistenId("MyPlayerService")
public MyPlayerService extends PlayerService {
    ...

    @NonNull
    @Override
    protected MusicPlayer onCreateMusicPlayer(@NonNull Context context, @NonNull MusicItem musicItem, @NonNull Uri uri) {
        return new ExoMusicPlayer(context, uri);
    }
}

提示:如果你对 MediaMusicPlayerExoMusicPlayer 都不满意,你完全可以通过实现 MusicPlayer 接口来创建一个自定义的音乐播放器实现。

2. 自定义通知栏控制器

如果你对播放器默认的通知栏控制器不满意,则可以覆盖 onCreateNotificationView() 方法并提供一个其他的 PlayerService.NotificationView 实现。

关于如何实现一个自定义的 NotificationView,参见:自定义 Notification

例:

@PersistenId("MyPlayerService")
public MyPlayerService extends PlayerService {
    ...
    
    @Nullable
    @Override
    protected NotificationView onCreateNotificationView() {
        return new MyCustomNotificationView();
    }
}

3. 自定义音频特效引擎

如果你的播放器需要支持自定义的音频特效(例如,均衡器),则可以覆盖 onCreateAudioEffectManager() 方法,并返回一个 AudioEffectManager 实现,该实现用于管理和同步音频特效的配置(另请参见:PlayerClient#setAudioEffectConfig(Bundle))。

本项目已经提供了一个音频特效引擎实现,具体请查看:EqualizerActivity

例:

@PersistenId("MyPlayerService")
public MyPlayerService extends PlayerService {
    ...
    
    @Nullable
    @Override
    protected AudioEffectManager onCreateAudioEffectManager() {
        return new AndroidAudioEffectManager();
    }
}

4. 记录播放历史

如果你需要记录播放器的播放历史,则可以覆盖 onCreateHistoryRecorder() 方法并返回一个 HistoryRecorder 实现。

例:

@PersistenId("MyPlayerService")
public MyPlayerService extends PlayerService {
    ...
    
    @Nullable
    @Override
    protected HistoryRecorder onCreateHistoryRecorder() {
        return new HistoryRecorder() {
            void recordHistory(MusicItem musicItem) {
                // ...记录播放历史
                // 该方法会在主线程中调用,如果你需要访问网络、数据库或者本地文件,请在后台线程中执行
            };
    }
}

5. 音质/动态 URL

默认情况下,onRetrieveMusicItemUri(MusicItem musicItem, SoundQuality soundQuality, AsyncResult<Uri> result) 方法仅仅只是调用 Uri.parse(String) 方法将 musicItem.getUri() 方法的返回值转换成一个 Uri 对象。如果你的播放器需要支持不同的音质或者动态 URL,则可以覆盖 onRetrieveMusicItemUri(MusicItem musicItem, SoundQuality soundQuality, AsyncResult<Uri> result) 方法来根据不同的 SoundQuality 枚举值返回歌曲对应音质的播放链接。该方法会在异步线程中执行,因此可以在该方法中执行耗时操作,例如访问网络或者本地数据库。

例:

@PersistenId("MyPlayerService")
public MyPlayerService extends PlayerService {
    ...

    @Override
    protected void onRetrieveMusicItemUri(@NonNull MusicItem musicItem, 
                                     @NonNull SoundQuality soundQuality
                                     @NonNull AsyncResult<Uri> result) throws Exception {
        // ...访问服务器获取 URL
        // 需要调用 result 参数的 onSuccess(T) 方法来返回异步任务的执行结果
        // 如果异步任务出错,则需要调用 result 参数的 onError(Throwable) 方法发出错误通知
        result.onSuccess(uri);
    }
}

这个异步任务可能会被取消,如果你需要监听它的取消事件,可以调用 AsyncResult#setOnCancelListener(AsyncResult.OnCancelListener listener) 方法向 AsyncResult<T> 注册一个 AsyncResult.OnCancelListener 监听器。

6. 仅 Wifi 网络播放

isCached(MusicItem, SoundQuality, AsyncResult<Boolean>) 方法用于判断具有特定 SoundQualityMusicItem 是否已缓存,该方法的默认结果为 false。如果你打算播放来自网络的音乐,并且打算支持 “仅 Wifi 网络播放” 功能,那么请务必覆盖该方法,因为 “仅 Wifi 网络播放” 功能需要使用该方法来判断歌曲是否已缓存。该方法会在异步线程中执行,因此可以在该方法中执行耗时操作,例如访问本地数据库。

例:

@PersistenId("MyPlayerService")
public MyPlayerService extends PlayerService {
    ...
    
    @Override
    protected void isCached(MusicItem musicItem, SoundQuality soundQuality, AsyncResult<Boolean> result) {
        // ...查询具有指定 soundQuality 的 musicItem 是否已缓存
        // 需要调用 result 参数的 onSuccess(T) 方法来返回异步任务的执行结果
        // 如果异步任务出错,则需要调用 result 参数的 onError(Throwable) 方法发出错误通知
        result.onSuccess(false);
    }
}

这个异步任务可能会被取消,如果你需要监听它的取消事件,可以调用 AsyncResult#setOnCancelListener(AsyncResult.OnCancelListener listener) 方法向 AsyncResult<T> 注册一个 AsyncResult.OnCancelListener 监听器。

7. 线控播放

onHeadsetHookClicked(int clickCount) 方法会在耳机按钮被点击时调用,用来实现线控播放,该方法的 clickCount 参数是连续点击的次数。默认情况下,点击 1 次是 “播放/暂停”;点击 2 次是 “下一曲”;点击 3 下是 “上一曲”。如果你对默认行为不满意,则可以覆盖该方法来自定义耳机按钮的点击行为。

例:

@PersistenId("MyPlayerService")
public MyPlayerService extends PlayerService {
    ...

    @Override
    protected void onHeadsetHookClicked(int clickCount) {
        // ...自定义线控播放
    }
}

8. 自定义 MediaSessionCompat.Callback

如果你需要对 MediaSessionCompat.Callback 进行自定义,则可以覆盖 onCreateMediaSessionCallback() 方法。该方法的返回值类型为 PlayerService.MediaSessionCallback,它是 MediaSessionCompat.Callback 的子类型。建议在覆盖 PlayerService.MediaSessionCallback 的方法时使用 super.xxx 回调超类对应的方法,否则本项目的部分功能将无法正常使用。

例:

@PersistenId("MyPlayerService")
public MyPlayerService extends PlayerService {
    ...

    @NonNull
    @Override
    protected MediaSessionCallback onCreateMediaSessionCallback() {
        // 返回自定义的 MediaSessionCallback
        return new MyMediaSessionCallback();
    }

    private static class MyMediaSessionCallback extends MediaSessionCallback {
        // ...
    }
}

其他

添加自定义动作

可以使用 addCustomAction(String, PlayerService.CustomAction) 方法添加自定义动作:

addCustomAction(
        String action,                             // 一个符合 Android 的 Action 命名规则的字符串,不能为 null
        PlayerService.CustomAction customAction    // action 被触发时要执行的动作,不能为 null
)

其中,action 参数是一个符合 AndroidAction 命名规则的字符串(如 "snow.player.debug.action.HELLO");customAction 是在 action 被触发时要执行的动作(这两个参数都不能为 null)。

例:

// 自定义动作的 action 字符串
public static final String ACTION_HELLO = "snow.player.debug.action.HELLO";

...

// 添加一个自定义动作
addCustomAction(ACTION_HELLO, new PlayerService.CustomAction() {
    @Override
    public void doAction(Player player, Bundle extras) {
        // ...
    }
});

触发自定义动作

共有 2 种触发自定义动作的方法:

  • 第 1 种:通过 sendBroadcast(Intent) 方法触发。
  • 第 2 种:通过调用 MediaControllerCompat.TransportControlssendCustomAction (String action, Bundle args) 触发。

例 1:

// 创建一个广播 Intent
Intent broadcastIntent = PlayerService.buildCustomActionIntent(ACTION_HELLO, PlayerService.class/*或者是 PlayerService 的任意一个子类型*/);

// 触发自定义动作
context.sendBroadcast(intent);

例 2:

// 通过 PlayerClient 对象获取到一个 MediaControllerCompat.TransportControls 对象
MediaControllerCompat.TransportControls controls = playerClient.getMediaController().getTransportControls();

// 要携带的额外参数
Bundle extras = new Bundle();

// 触发自定义动作
controls.sendCustomAction(ACTION_HELLO, extras);

End