Skip to content

[ZH] 2.进阶

jrfeng edited this page Feb 19, 2023 · 18 revisions

目录:

建立连接

PlayerClient 是播放器的客户端,PlayerService 则是播放器的服务端。在使用 PlayerClient 前,需要先建立与 PlayerService 服务端的连接。

当然,在使用 PlayerClient 连接到服务端前,需要先创建一个 PlayerClient 对象。可以使用静态方法 PlayerClient.newInstance(Context, Class<? extends PlayerService>) 创建一个 PlayerClient 对象。

public static PlayerClient newInstance(
        Context context,                                // Context 对象,不能为 null
        Class<? extends PlayerService> playerService    // PlayerClient 将要连接到的那个 PlayerService 的 Class 对象,不能为 null
)

其中,第 1 参数是一个 Context 对象,不能为 null;第 2 个参数是一个 Class 对象,这是 PlayerClient 将要连接到的那个 PlayerServiceClass 对象,同样不能为 null

创建 PlayerClient 对象后,可以调用它的以下方法建立与 PlayerService 的连接:

  • connect()
  • connect(PlayerClient.OnConnectCallback callback)

其中,第 2 个 connect 方法可以接收一个 PlayerClient.OnConnectCallback 类型的参数,该参数是一个回调接口,该回调接口会在连接成功或失败时调用,如果你需要知道连接结果,则可以使用该方法。

调用 connect() 方法时,如果 PlayerService 还没有启动或者已经终止,则会启动 PlayerService,然后与其建立连接。

例:

PlayerClient playerClient = PlayerClient.newInstance(context, MyPlayerService.class);

playerClient.connect(new PlayerClient.OnConnectCallback() {
    @Override
    public void onConnected(boolean success) {
        // DEBUG
        Log.d("DEBUG", "connect result: " + success);
    }
});

此外,还可以使用 PlayerClientisConnected() 来检查连接结果,如果已经连接成功,则该方法会返回 true,如果没有连接、连接失败或者已断开连接,则该方法会返回 false

播放列表

设置播放列表

可以调用 PlayerClient 的以下方法来设置一个新的播放列表:

  • setPlaylist(Playlist playlist):设置一个新的播放列表。
  • setPlaylist(Playlist playlist, boolean play):设置一个新的播放列表,并决定是否立即播放列表中的第 0 首音乐。
  • setPlaylist(Playlist playlist, int position, boolean play):设置一个新的播放列表,同时使用 position 参数指定要播放的歌曲的位置,并决定是否立即播放第 position 首音乐(position 是歌曲在播放列表中的索引,从 0 开始计算)。

注意!不要设置过大的播放列表(建议在 1000 以内),过大的播放列表会导致 Binder 崩溃!

获取播放列表

连接成功后,可以调用 PlayerClient 的以下方法获取播放器的播放列表:

  • getPlaylist(PlaylistManager.Callback callback):获取当前的播放队列。
  • getPlaylistSize():获取播放列表的大小(包含的歌曲的数量)。

例:

playerClient.getPlaylist(new PlaylistManager.Callback() {
    @Override
    public void onFinished(Playlist playlist) {
        // ...
    }
});

编辑播放列表

连接成功后,可以调用 PlayerClient 的以下方法编辑播放器的播放列表:

  • setNextPlay(MusicItem musicItem):设置下一首播放的歌曲。如果列表中已包含指定歌曲,则会将它移动到下一首要播放的位置,否则会将 MusicItem 插入到下一首要播放的位置。
  • insertMusicItem(int position, MusicItem musicItem):将一首歌曲插入到播放列表的指定位置。如果播放列表中已包含指定歌曲,则会将它移动到 position 位置。
  • appendMusicItem(MusicItem musicItem):将一首歌曲追加到播放列表的末尾。如果播放列表中已包含指定歌曲,则会将它移动到播放列表的末尾。
  • moveMusicItem(int fromPosition, int toPosition):将播放列表中 fromPosition 位置处的歌曲移动到 toPosition 位置。
  • removeMusicItem(MusicItem musicItem):从播放列表中移除指定歌曲。如果播放列表中不包含该歌曲,则忽略本次调用。

监听播放列表的改变

如果你需要监听播放列表的改变,可以调用 PlayerClientaddOnPlaylistChangeListener(Player.OnPlaylistChangeListener listener) 注册一个 Player.OnPlaylistChangeListener 监听器来监听播放列表的改变。当播放列表发生改变时,该监听器的 onPlaylistChanged(PlaylistManager playlistManager, int position) 方法会被调用,其中,PlaylistManager 可以用来获取最新的播放列表,参数 position 是当前正在播放的歌曲在播放列表中的位置(从 0 开始计数)。

例:

playerClient.addOnPlaylistChangeListener(new Player.OnPlaylistChangeListener() {
    @Override
    public void onPlaylistChanged(PlaylistManager playlistManager, int position) {
        // get fresh playlist
        playlistManager.getPlaylist(new PlaylistManager.Callback() {
            @Override
            public void onFinished(Playlist playlist) {
                // ...
            }
        });
        
        // ...
    }
});

如果你需要在监听到播放列表改变时获取最新的播放列表,可以使用 PlaylistLiveData 来监听并获取最新的播放列表。使用 PlaylistLiveData 的好处是无需担心会发生内存泄露,而且与使用监听器相比,开发者可以编写更少的代码。

例:

PlaylistLiveData playlistLiveData = new PlaylistLiveData();
playlistLiveData.init(playerClient);

playlistLiveData.observe(lifecycleOwner, new Observer<Playlist>() {
    @Override
    public void onChanged(Playlist playlist) {
        // ...
    }
});

控制播放器

可以调用 PlayerClient 的以下方法来控制播放器:

  • play():播放
  • pause():暂停
  • playPause():播放/暂停
  • playPause(int position):播放/暂停列表中指定位置处的音乐
  • stop():停止
  • seekTo(int progress):调整播放位置
  • skipToPrevious():上一曲
  • skipToNext():下一曲
  • skipToPosition(int position):播放列表中 position 处的音乐,如果正在播放,则忽略本次调用
  • fastForward():快进
  • rewind():快退

更多方法,请查看 API 文档

配置播放器

可以调用 PlayerClient 的以下方法对播放器进行配置:

  • setPlayMode(PlayMode playMode):设置播放模式(共有 3 种模式:顺序播放、单曲循环、随机播放)
  • setOnlyWifiNetwork(boolean onlyWifiNetwork):设置是否只允许在 WiFi 网络下播放音乐(默认为 false
  • setSoundQuality(SoundQuality soundQuality):设置播放器的首选音质
  • setAudioEffectEnabled(boolean enabled):设置是否启用音频特效(如:均衡器,默认为 false
  • setAudioEffectConfig(Bundle config):修改音频特效的配置

注意!“仅 WiFi 网络播放”、“播放器的首选音质” 与 “音频特效” 功能需要开发者配合 PlayerService 自行实现,具体请参考:自定义 PlayerService

关于音频特效,请查看 EqualizerActivity

监听播放器状态

PlayerClient 提供了众多方法来帮助开发者监听播放器的状态:

  1. addOnPlaybackStateChangeListener(PlayerClient.OnPlaybackStateChangeListener listener):监听播放器状态(粗粒度)
  2. addOnPlaybackStateChangeListener(Player.OnPlaybackStateChangeListener listener):监听播放器状态(细粒度)
  3. addOnPrepareListener(Player.OnPrepareListener listener):监听播放器准备(prepare)状态
  4. addOnBufferedProgressChangeListener(Player.OnBufferedProgressChangeListener listener):监听播放器缓存进度
  5. addOnAudioSessionChangeListener(PlayerClient.OnAudioSessionChangeListener listener):监听播放器的 audio session id
  6. addOnPlayingMusicItemChangeListener(Player.OnPlayingMusicItemChangeListener listener):监听正在播放的 MusicItem
  7. addOnPlaylistChangeListener(Player.OnPlaylistChangeListener listener):监听播放器正在播放的列表
  8. addOnPlayModeChangeListener(Player.OnPlayModeChangeListener listener):监听播放器的播放模式
  9. addOnSeekCompleteListener(Player.OnSeekCompleteListener listener):监听播放器播放进度调整完成事件
  10. addOnConnectStateChangeListener(PlayerClient.OnConnectStateChangeListener listener):监听 PlayerClient 的连接成功断开连接事件
  11. addOnStalledChangeListener(Player.OnStalledChangeListener listener):监听播放器的 stalled 状态。当播放的缓冲区没有足够的数据支持继续播放时,stalled 状态为 true,否则为 false
  12. addOnRepeatListener(Player.OnRepeatListener listener):当将播放模式设置为循环播放(PlayMode.LOOP)时,会在歌曲播放完,准备再次循环播放时调用该方法

当你不再需要某个监听器时,可以调用对应的 removeXxx 方法将其移除。

例:

Player.OnPlaybackStateChangeListener listener = new Player.OnPlaybackStateChangeListener() {
    // ...
};

// 添加监听器:Player.OnPlaybackStateChangeListener
playerClient.addOnPlaybackStateChangeListener(listener);

// 移除监听器:Player.OnPlaybackStateChangeListener
playerClient.removeOnPlaybackStateChangeListener(listener);

上面这些方法各自都有 1 个接收 LifecycleOwner 作为第 1 个参数的重载方法,使用这些重载方法添加的监听器会在 LifecycleOwner 销毁时自动注销以避免内存泄露。

public void addOnPlaybackStateChangeListener(Player.OnPlaybackStateChangeListener listener)

// 接收 LifecycleOwner 作为第一个参数
public void addOnPlaybackStateChangeListener(LifecycleOwner owner,
                                             Player.OnPlaybackStateChangeListener listener)

例:

Player.OnPlaybackStateChangeListener listener = new Player.OnPlaybackStateChangeListener() {
    // ...
};

playerClient.addOnPlaybackStateChangeListener(lifecycleOwner, listener);

获取播放器状态

除了使用监听器来监听播放器的状态外,还可以手动调用 PlayerClient 的各种 getter 方法来获取播放器状态。其中常用的方法有:

  • getPlayingMusicItem():获取当前正在播放的音乐。
  • getPlaybackState():获取当前播放状态。
  • getPlayPosition():获取当前播放列表的播放位置。
  • getPlayProgress():获取播放进度(单位:毫秒)。
  • getPlayProgressUpdateTime():获取播放进度的更新时间。注意!这是基于 SystemClock.elapsedRealtime() 的时间
  • getPlayMode():获取当前播放模式。
  • getErrorCode():获取错误码。
  • getErrorMessage():获取错误信息。
  • getAudioSessionId():获取正在播放的歌曲的 audio session id。如果没有播放任何歌曲,则返回 0
  • getBufferedProgress():获取当前的缓存进度。

提示:这些用于获取播放器状态的方法并不是 “实时” 的。例如,如果你在调用了 skipToNext() 方法后立即去调用 getPlayingMusicItem() 方法,那么有很大概率获取到的并不是当前正在播放的歌曲,这是因为像 skipToNext() 这类用来控制播放器的方法仅仅只是在发出一条命令后立即返回,并不会等到命令执行完毕后再返回。因此,如果你在 skipToNext() 方法返回后立即调用上面这些方法获取播放器状态,由于此时命令可能还没有执行完毕(或者根本没有执行),因此获取到的状态可能并不是你所期望的。如果你需要准确的获取播放器的状态,则应该使用上一节介绍的监听器,这些监听器会在播放器的状态改变时立即被调用。

更多方法,请查看 API 文档

PlayerViewModel

使用监听器来监听播放器状态并更新 UI 是一个非常繁琐的过程。为了简化这个过程,本项目为此提供了一个 PlayerViewModel

PlayerViewModel 的使用方法与其他的 ViewModel 区别不大。只需在创建好 PlayerViewModel 对象后调用它的任意一个 init 方法对其进行初始化即可(如:init(Context, PlayerClient))。

例:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    PlayerViewModel playerViewModel = new ViewModelProvider(this).get(PlayerViewModel.class);

    // 避免重复初始化和创建多余的 PlayerClient 对象
    if (playerViewModel.isInitialized()) {
        mPlayerClient = playerViewModel.getPlayerClient();
        return;
    }

    // 创建 PlayerClient 对象
    mPlayerClient = PlayerClient.newInstance(this, MyPlayerService.class);

    // 初始化 PlayerViewModel 对象
    playerViewModel.init(this, mPlayerClient);

    // 设置在 ViewModel 被销毁时自动断开 PlayerClient 的连接
    playerViewModel.setDisconnectOnCleared(true);
}

断开连接

最后,当不再需要 PlayerClient 对象时,可以调用它的 disconnect() 方法断开与 PlayerService 的连接。PlayerService 会继续保持在后台运行,并不会因所有客户端都断开连接而终止运行。断开连接后的 PlayerClient 对象可以重复使用,你可以再次调用它的 connect() 方法重新建立与播放器的连接。

例:

// 断开连接
mPlayerClient.disconnect();

...

// 断开连接后,可以再次 connect 方法重新建立连接
mPlayerClient.connect();

终止 PlayerService

如果你需要终止 PlayerService,可以调用 PlayerClientshutdown() 方法,调用该方法后,PlayerService 会被终止,所有客户端的连接都会被断开。如果你希望再次运行播放器,则需要重新调用 connect() 方法进行连接,因为 PlayerService 已被终止了,调用 connect() 方法可以重新启动它。

例:

// 终止 PlayerService
mPlayerClient.shutdown();

// 终止 PlayerService 后,所有客户端的连接都会被断开

// 如需再次运行播放器,需要重新调用 connect() 方法连接并启动 PlayerService
mPlayerClient.connect();

下载与缓存

ExoPlayer 提供了对下载与缓存的支持,此框架提供了对 ExoPlayer 的支持,你可以在此框架中快速集成 ExoPlayer,详情请参阅 Wiki:使用 ExoPlayer

关于下载与缓存,请参考 ExoPlayer 文档:Downloading media

其他内容

睡眠定时器

本项目提供了 “睡眠定时器” 功能,你可以设置一个定时时间(单位:毫秒),并设置当定时时间到时要执行的操作(暂停播放、停止播放或者关闭 PlayerService)。

可以调用 PlayerClient 的以下方法启动或停止睡眠定时器:

  • startSleepTimer(long time):启动睡眠定时器,当定时时间到时将暂停播放。
  • startSleepTimer(long time, SleepTimer.TimeoutAction action):启动睡眠定时器,当定时时间到时执行由 action 参数指定的操作。共有 3 种操作:
    • SleepTimer.TimeoutAction.PAUSE:当定时时间到时暂停播放。
    • SleepTimer.TimeoutAction.STOP:当定时时间到时停止播放。
    • SleepTimer.TimeoutAction.SHUTDOWN:当定时时间到时关闭 PlayerService
  • cancelSleepTimer():取消睡眠定时器。

可以调用 PlayerClient 的以下方法获取睡眠定时器的状态:

  • isSleepTimerStarted():查询睡眠定时器是否已启动,如果已经启动,则返回 true,否则返回 false
  • getSleepTimerTime():获取睡眠定时器的定时时间(单位:毫秒)。该方法的返回值只在睡眠定时器已启动时才有意义。
  • getSleepTimerStartedTime():获取睡眠定时器的启动时间(单位:毫秒, 基于 SystemClock.elapsedRealtime())。该方法的返回值只在睡眠定时器已启动时才有意义。
  • getSleepTimerElapsedTime():获取睡眠定时器已经走过的时间(单位:毫秒)。该方法的返回值只在睡眠定时器已启动时才有意义。

此外,还可以调用 PlayerClient 的以下方法注册一个监听器来监听睡眠定时器的状态:

  • addOnSleepTimerStateChangeListener(SleepTimer.OnStateChangeListener listener):监听睡眠定时器的状态。
  • addOnSleepTimerStateChangeListener(LifecycleOwner owner, SleepTimer.OnStateChangeListener listener):监听睡眠定时器的状态。额外接收一个 LifecycleOwner,可以避免内存泄露。
  • removeOnSleepTimerStateChangeListener(SleepTimer.OnStateChangeListener listener):移除已注册的睡眠定时器状态监听器。

SleepTimer.OnStateChangeListener 监听器的回调方法:

  • onTimerStart(long time, long startTime, SleepTimer.TimeoutAction action):当启动睡眠定时器时会调用该方法。
  • onTimerEnd():当睡眠定时器正常终止(定时时间到)或者被取消时会调用该方法。

最大空闲时间

你可以在你的 PlayerServiceonCreate 方法中调用 setMaxIDLETime(int minutes) 方法设置当前 PlayerService 的最大空闲时间(单位:分钟)。

当播放器 没有 处于 PlaybackState.PLAYING 状态且 preparingstalled 都为 false 时,就将 PlayerService 视为处于空闲状态。当 PlayerService 处于空闲状态的时间超过使用 setMaxIDLETime(int minutes) 方法设置的最长时间时,PlayerService 将自动终止。这可以节省系统资源,并节约电池电量。

默认的最大空闲时间为 -1。当设置的最大空闲时间小于等于 0 时,将关闭该功能,也就是说默认没有启用该功能。

例:

public class MyPlayerService extends PlayerService {
    ...

    @Override
    public void onCreate() {
        super.onCreate();

        // 设置当前 PlayerService 的最大空闲时间
        setMaxIDLETime(10);
        ...
    }
    ...
}

禁用 seek 操作

如果你的音频源不支持 seek 操作,例如直播流(Live Stream),则应该在创建 MusicItem 对象时禁用 seek 操作。

可以在使用 MusicItem.Builder 构建 MusicItem 对象时调用 MusicItem.Builder#setForbidSeek(boolean) 方法禁用 seek 操作。也可以在创建 MusicItem 对象后调用 MusicItem#setForbidSeek(boolean) 方法禁用 seek 操作。

例:

MusicItem liveStream = new MusicItem.Builder()
            .setTitle("Live Stream")
            .setArtist("Live Stream")
            .setDuration(0)    // 或者 Integer.MAX_VALUE
            .setUri("https://www.example.com/some_live_stream")
            .setIconUri("https://www.example.com/icon.png")

            // 禁用 seek 操作
            .setForbidSeek(true)

            .build();

忽略音频焦点

你可以调用以下方法来设置是否忽略音频焦点:

还可以使用以下方法判断是否忽略音频焦点:

例:

public void toggleIgnoreAudioFocus() {
    mPlayerClient.setIgnoreAudioFocus(!mPlayerClient.isIgnoreAudioFocus());
}

如果忽略了音频焦点,则在其他音频应用开始播放音频时不会自动暂停播放。

警告!

警告!忽略音频焦点后,来电时播放器不会自动暂停播放!如果您想忽略音频焦点,并在来电时自动暂停播放,则需要在运行时请求 android.permission.READ_PHONE_STATE 权限。

例:

<uses-permission android:name="android.permission.READ_PHONE_STATE" />

在运行时申请权限:

boolean noPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED;

if (noPermission) {
    ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.READ_PHONE_STATE}, requestCode);
}

MediaSession 框架

本项目是基于 MediaSession 框架构建的,并提供了对 MediaSession 框架的兼容。

要点:

更多内容,请查看 API 文档

AppWidget

该项目提供了对 AppWidget 的支持,开发者可以在 AppWidgetProvider 中使用 AppWidgetPlayerState 类来获取播放器状态。

为了在播放器状态变化后刷新 AppWidget,请重载 PlayerServicegetAppWidget() 方法,并在该方法中返回你的 AppWidgetProviderClass 对象。

例:

public class MyPlayerService extends PlayerService {
    // ...

    @Nullable
    @Override
    protected Class<? extends AppWidgetProvider> getAppWidget() {
        return ExampleAppWidgetProvider.class;
    }
}

如果你的应用中有多个 AppWidget 需要刷新,可以重载 getAppWidgets() 方法。如果重载了 getAppWidgets() 方法并且返回了一个非 null 值,则 getAppWidget() 方法将不再有效。

例:

public class MyPlayerService extends PlayerService {
    // ...

    @Nullable
    @Override
    protected List<Class<? extends AppWidgetProvider>> getAppWidgets() {
        List<Class<? extends AppWidgetProvider>> appWidgets = new ArraysList<>();
        
        appWidgets.add(ExampleAppWidgetProviderA.class);
        appWidgets.add(ExampleAppWidgetProviderB.class);
        appWidgets.add(ExampleAppWidgetProviderC.class);
    
        return appWidgets;
    }
}

获取播放器状态:

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);

        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
        // 获取播放器状态
        AppWidgetPlayerState state = AppWidgetPlayerState.getPlayerState(context, MyPlayerService.class);
        if (state == null) {
            return;
        }

        // ...
    }
}

通过 ACTION_PLAYER_STATE_CHANGED 更新 AppWidget

当播放器状态改变时,会发送一个 ACTION_PLAYER_STATE_CHANGED"snow.player.appwidget.action.PLAYER_STATE_CHANGED")广播,该广播的 Category 是你的 PlayerService 的完整类名(如 snow.demo.MyPlayerService)。可以为你的 AppWidgetProvider 添加一个监听该广播的 <intent-filter>,并在接收到该广播时更新你的 AppWidget

注意!由于 Android 限制了隐式广播的发送,因此该方法在高版本 Android 系统上不一定有效,建议优先使用上面介绍的第一种方法。

例:

首先,在你的应用的 AndroidManifest.xml 声明并使用以下权限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="snow.player.debug">

    <!--声明并使用权限-->
    <permission android:name="snow.player.appwidget.permission.UPDATE_APPWIDGET" />
    <uses-permission android:name="snow.player.appwidget.permission.UPDATE_APPWIDGET" />

    ...

</manifest>

然后,为你的 AppWidgetProvider 增加一个 <intent-filter> 元素:

<receiver
    android:name=".ExampleAppWidgetProvider"
    android:exported="true">

    ...

    <!--增加一个 intent-filter 元素-->
    <intent-filter>
        <action android:name="snow.player.appwidget.action.PLAYER_STATE_CHANGED" />
        
        <!-- 该 category 的 android:name 是你的 PlayerService 的完整类名 -->
        <category android:name="snow.player.debug.MyPlayerService" />
    </intent-filter>
</receiver>

最后,在接收到广播时更新 AppWidget

public class ExampleAppWidgetProvider extends AppWidgetProvider {
    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);

        // 接收到广播时更新 AppWidget
        if (AppWidgetPlayerState.ACTION_PLAYER_STATE_CHANGED.equals(intent.getAction())) {
            AppWidgetManager am = AppWidgetManager.getInstance(context);
            onUpdate(context, am, am.getAppWidgetIds(new ComponentName(context, ExampleAppWidgetProvider.class)));
        }
    }
    
    // ...
    
}

可以在 AppWidgetProvideronUpdate 方法中使用静态方法 AppWidgetPlayerState.getPlayerState(Context, Class<? extends PlayerService>) 获取到最新的播放器状态。

public static AppWidgetPlayerState getPlayerState(
        Context context, 
        Class<? extends PlayerService> playerService
)

参数说明:

  • contextContext 对象,不能为 null
  • playerService:你的 PlayerService 的 Class 对象,不能为 null

返回值类型:

  • AppWidgetPlayerState:该类型提供了一系列获取播放器状态的 getter 方法,通过该类型即可获取到最新的播放器状态。

例:

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);

        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
        // 获取播放器状态
        AppWidgetPlayerState state = AppWidgetPlayerState.getPlayerState(context, MyPlayerService.class);
        if (state == null) {
            return;
        }

        // ...
    }
}

End