DistributedMusicPlayer分布式音乐播放器
介绍
本示例主要演示了如何通过迁移数据进行音乐的分布式播放。实现了音乐播放的跨设备迁移,包括:播放哪首歌曲、播放进度、以及播放状态的保持。
效果展示
搭建环境
安装DevEco Studio,详情请参考DevEco Studio下载。
设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
下载源码,导入项目。
代码结构
- config.json #全局配置文件
- │
- ├─java
- │ └─ohos
- │ └─samples
- │ └─distributedmusicplayer
- │ │ MainAbility.java
- │ │
- │ ├─slice
- │ │ MainAbilitySlice.java #播放器主能力Slice
- │ │
- │ └─utils
- │ LogUtil.java #日志工具类
- │ PlayerManager.java #播放器管理者
- │ PlayerStateListener.java #播放器状态监听器
- │
- └─resources
- ├─base
- │ ├─element
- │ │ string.json
- │ │
- │ ├─graphic
- │ │ button_bg.xml
- │ │
- │ ├─layout
- │ │ main_ability_slice.xml #播放器页面布局
- │ │
- │ └─media #海报、按钮图片资源
- │ album.png
- │ album2.png
- │ bg_blurry.png
- │ icon.png
- │ ic_himusic_next.png
- │ ic_himusic_pause.png
- │ ic_himusic_play.png
- │ ic_himusic_previous.png
- │ remote_play_selected.png
- │
- └─rawfile #歌曲媒体资源
- Homey.mp3
- Homey.wav
- Technology.mp3
- Technology.wav
实现步骤
1.实现跨设备迁移标准步骤,参见HarmonyOS Sample 之 AbilityInteraction设备迁移
2.实现一个播放器管理者PlayerManager
2.1.定义播放器的状态,包括: 播放、暂停、完成、播放中
- private static final int PLAY_STATE_PLAY = 0x0000001;
- private static final int PLAY_STATE_PAUSE = 0x0000002;
- private static final int PLAY_STATE_FINISH = 0x0000003;
- private static final int PLAY_STATE_PROGRESS = 0x0000004;
2.2.实现基本的方法,包括:播放、暂停、切换歌曲、更新播放进度方法
还有一些辅助方法,包括:设置媒体资源、定时更新播放进度、获取播放总时长、
要用到Player/Timer/自定义的PlayerStateListener/EventHandler事件处理/PlayCallBack播放器回调类
- /**
- * play
- */
- public void play() {
- try {
- if (!isPrepared) {
- LogUtil.error(TAG, "prepare fail");
- return;
- }
- //如果开始播放则返回真; 否则返回 false。
- if (!musicPlayer.play()) {
- LogUtil.error(TAG, "play fail");
- return;
- }
- startTask();
- handler.sendEvent(PLAY_STATE_PLAY);
- } catch (IllegalArgumentException e) {
- LogUtil.error(TAG, e.getMessage());
- e.printStackTrace();
- }
- }
- /**
- * pause
- */
- public void pause() {
- if (!musicPlayer.pause()) {
- LogUtil.info(TAG, "pause fail");
- return;
- }
- //停止计时
- finishTask();
- //
- handler.sendEvent(PLAY_STATE_PAUSE);
- }
- /**
- * switch music
- *
- * @param uri music uri
- */
- public void switchMusic(String uri) {
- currentUri = uri;
- //设置资源
- setResource(currentUri);
- //播放
- play();
- }
- /**
- * changes the playback position
- * 更新当前播放进度
- *
- * @param currentTime current time
- */
- public void rewindTo(int currentTime) {
- musicPlayer.rewindTo(currentTime * 1000);
- }
- /**
- * set source
- *
- * @param uri music uri
- */
- public void setResource(String uri) {
- LogUtil.info(TAG, "setResource,uri: " + uri);
- try {
- RawFileEntry rawFileEntry = context.getResourceManager().getRawFileEntry(uri);
- BaseFileDescriptor baseFileDescriptor = rawFileEntry.openRawFileDescriptor();
- //LogUtil.info(TAG, "setResource,baseFileDescriptor : " + baseFileDescriptor);
- if (!musicPlayer.setSource(baseFileDescriptor)) {
- LogUtil.info(TAG, "uri is invalid");
- return;
- }
- //准备播放环境并缓冲媒体数据。
- isPrepared = musicPlayer.prepare();
- LogUtil.info(TAG, "setResource,isPrepared: " + isPrepared);
- //歌曲名称
- String listenerUri = currentUri.substring(currentUri.lastIndexOf("/") + 1, currentUri.lastIndexOf("."));
- playerStateListener.onUriSet(listenerUri);
- LogUtil.info(TAG, "setResource,listenerUri: " + listenerUri);
- } catch (IOException e) {
- LogUtil.error(TAG, "io exception");
- }
- }
- /**
- * 定时事件通知更新进度条
- */
- private void startTask() {
- LogUtil.debug(TAG, "startTask");
- finishTask();
- timerTask = new TimerTask() {
- @Override
- public void run() {
- handler.sendEvent(PLAY_STATE_PROGRESS);
- }
- };
- timer = new Timer();
- timer.schedule(timerTask, DELAY_TIME, PERIOD);
- }
- private void finishTask() {
- LogUtil.debug(TAG, "finishTask");
- if (timer != null && timerTask != null) {
- timer.cancel();
- timer = null;
- timerTask = null;
- }
- }
2.3.PlayerStateListener播放器状态监听器有如下方法:
onPlaySuccess播放成功时被调用
onPauseSuccess暂停时被调用
onPositionChange进度发生变化时被调用
onMusicFinished音乐播放完成时被调用
onUriSet资源被设置时被调用
- /**
- * PlayerStateListener
- */
- public interface PlayerStateListener {
- void onPlaySuccess(int totalTime);
- void onPauseSuccess();
- void onPositionChange(int currentTime);
- void onMusicFinished();
- void onUriSet(String name);
- }
2.4.PlayCallBack播放器回调类实现了Player.IPlayerCallback接口,实现了如下方法:
onPrepared 当媒体文件准备好播放时调用。
onMessage当收到播放器消息或警报时调用。
onError收到播放器错误消息时调用。
onResolutionChanged当视频大小改变时调用。
onPlayBackComplete播放完成时调用。
onRewindToComplete 当播放位置被 Player.rewindTo(long) 改变时调用。
onBufferingChange当缓冲百分比更新时调用。
onNewTimedMetaData当有新的定时元数据可用时调用。
onMediaTimeIncontinuity当媒体时间连续性中断时调用,例如播放过程中出现错误,播放位置被Player.rewindTo(long)改变,或者播放速度突然改变。
- /**
- * 在播放完成、播放位置更改和视频大小更改时提供媒体播放器回调。
- */
- private class PlayCallBack implements Player.IPlayerCallback {
- /**
- * 当媒体文件准备好播放时调用。
- */
- @Override
- public void onPrepared() {
- LogUtil.info(TAG, "onPrepared");
- }
- /**
- * 当收到播放器消息或警报时调用。
- *
- * @param type
- * @param extra
- */
- @Override
- public void onMessage(int type, int extra) {
- LogUtil.info(TAG, "onMessage " + type + "-" + extra);
- }
- /**
- * 收到播放器错误消息时调用。
- *
- * @param errorType
- * @param errorCode
- */
- @Override
- public void onError(int errorType, int errorCode) {
- LogUtil.info(TAG, "onError " + errorType + "-" + errorCode);
- }
- /**
- * 当视频大小改变时调用。
- *
- * @param width
- * @param height
- */
- @Override
- public void onResolutionChanged(int width, int height) {
- LogUtil.info(TAG, "onResolutionChanged " + width + "-" + height);
- }
- /**
- * 播放完成时调用。
- */
- @Override
- public void onPlayBackComplete() {
- //不会自动被调用????
- LogUtil.info(TAG, "onPlayBackComplete----------------");
- handler.sendEvent(PLAY_STATE_FINISH);
- }
- /**
- * 当播放位置被 Player.rewindTo(long) 改变时调用。
- */
- @Override
- public void onRewindToComplete() {
- LogUtil.info(TAG, "onRewindToComplete");
- }
- /**
- * 当缓冲百分比更新时调用。
- *
- * @param percent
- */
- @Override
- public void onBufferingChange(int percent) {
- LogUtil.info(TAG, "onBufferingChange:" + percent);
- }
- /**
- * 当有新的定时元数据可用时调用。
- *
- * @param mediaTimedMetaData
- */
- @Override
- public void onNewTimedMetaData(Player.MediaTimedMetaData mediaTimedMetaData) {
- LogUtil.info(TAG, "onNewTimedMetaData");
- }
- /**
- * 当媒体时间连续性中断时调用,例如播放过程中出现错误,播放位置被Player.rewindTo(long)改变,或者播放速度突然改变。
- *
- * @param mediaTimeInfo
- */
- @Override
- public void onMediaTimeIncontinuity(Player.MediaTimeInfo mediaTimeInfo) {
- LogUtil.info(TAG, "onNewTimedMetaData");
- }
- }
3.MainAbilitySlice 中 implements PlayerStateListener , IAbilityContinuation接口
- public class MainAbilitySlice extends AbilitySlice implements PlayerStateListener, IAbilityContinuation {
- ...
3.1.实现PlayerStateListener接口方法
- @Override
- public void onPlaySuccess(int totalTime) {
- LogUtil.debug(TAG, "onPlaySuccess");
- //设置图标
- musicPlayButton.setPixelMap(ResourceTable.Media_ic_himusic_pause);
- //设置总时长文本
- this.totalTimeText.setText(getTime(totalTime));
- //设置进度条
- slider.setMaxValue(totalTime);
- //设置当前歌曲海报
- musicPosters.setPixelMap(posters[currentPos]);
- }
- @Override
- public void onPauseSuccess() {
- LogUtil.debug(TAG, "onPauseSuccess");
- //设置图标
- musicPlayButton.setPixelMap(ResourceTable.Media_ic_himusic_play);
- }
- @Override
- public void onUriSet(String name) {
- LogUtil.debug(TAG, "onUriSet");
- //设置歌曲名称
- musicNameText.setText(name);
- }
- @Override
- public void onPositionChange(int currentTime) {
- if(currentTime < totalTime){
- LogUtil.info(TAG, "onPositionChange currentTime = " + currentTime+",totalTime="+totalTime);
- this.currentTime = currentTime;
- //设置播放时间文本
- this.currentTimeText.setText(getTime(currentTime));
- //设置进度条的当前播放时间
- slider.setProgressValue(currentTime);
- }else{
- LogUtil.info(TAG, "onPositionChange, current song end");
- //设置播放器图标
- musicPlayButton.setPixelMap(ResourceTable.Media_ic_himusic_play);
- }
- }
- /**
- *音乐播放完成时应该被调用,但是没被调用
- */
- @Override
- public void onMusicFinished() {
- //TODO???????????
- LogUtil.debug(TAG, "onMusicFinished");
- currentPos = currentPos == 0 ? 1 : 0;
- currentUri = musics[currentPos];
- //切换歌曲
- playerManager.switchMusic(currentUri);
- //总时长
- totalTime=playerManager.getTotalTime();
- }
3.2.实现IAbilityContinuation接口方法
- @Override
- public boolean onStartContinuation() {
- LogUtil.debug(TAG, "onStartContinuation");
- return true;
- }
- @Override
- public boolean onSaveData(IntentParams intentParams) {
- LogUtil.debug(TAG, "onSaveData");
- //
- intentParams.setParam(KEY_CURRENT_TIME, currentTime);
- intentParams.setParam(KEY_POSITION, currentPos);
- intentParams.setParam(KEY_PLAY_STATE, String.valueOf(playerManager.isPlaying()));
- LogUtil.info(TAG, "onSaveData:" + currentTime);
- return true;
- }
- @Override
- public boolean onRestoreData(IntentParams intentParams) {
- LogUtil.debug(TAG, "onRestoreData");
- if (!(intentParams.getParam(KEY_POSITION) instanceof Integer)) {
- return false;
- }
- if (!(intentParams.getParam(KEY_CURRENT_TIME) instanceof Integer)) {
- return false;
- }
- if (!(intentParams.getParam(KEY_PLAY_STATE) instanceof String)) {
- return false;
- }
- //恢复数据,获取迁移过来的参数:播放位置、时间和播放状态
- currentPos = (int) intentParams.getParam(KEY_POSITION);
- currentTime = (int) intentParams.getParam(KEY_CURRENT_TIME);
- Object object = intentParams.getParam(KEY_PLAY_STATE);
- if (object instanceof String) {
- isPlaying = Boolean.parseBoolean((String) object);
- }
- isInteractionPlay = true;
- LogUtil.info(TAG, "onRestoreData:" + currentTime);
- return true;
- }
- @Override
- public void onCompleteContinuation(int i) {
- terminate();
- }
3.3.定义ValueChangedListenerImpl进度值变化的监听事件
实现 Slider.ValueChangedListener 接口方法
- /**
- *进度条值变化的监听事件
- */
- private class ValueChangedListenerImpl implements Slider.ValueChangedListener {
- @Override
- public void onProgressUpdated(Slider slider, int progress, boolean fromUser) {
- currentTime = progress;
- }
- @Override
- public void onTouchStart(Slider slider) {
- LogUtil.debug(TAG, "onTouchStart");
- }
- @Override
- public void onTouchEnd(Slider slider) {
- LogUtil.debug(TAG, "onTouchEnd");
- //快速更改播放进度
- playerManager.rewindTo(currentTime);
- //当前播放时间
- currentTimeText.setText(getTime(currentTime));
- }
- }
3.4.定义迁移数据的KEY,音乐当前的播放时间、播放的歌曲索引(位置)、播放状态
- private static final String KEY_CURRENT_TIME = "main_ability_slice_current_time";
- private static final String KEY_POSITION = "main_ability_slice_position";
- private static final String KEY_PLAY_STATE = "main_ability_slice_play_state";
- private int currentPos = 0;
- private String currentUri;
- //是否是互动播放,true表示远端迁移恢复的
- private boolean isInteractionPlay;
- private int currentTime;
- //当前播放歌曲总时长
- private int totalTime;
- private boolean isPlaying;
3.5.定义播放的音乐URI,这里准备了2首,还有对应的海报
- private static final String URI1 = "resources/rawfile/Technology.wav";
- private static final String URI2 = "resources/rawfile/Homey.wav";
- private final String[] musics = {URI1, URI2};
- private final int[] posters = {ResourceTable.Media_album, ResourceTable.Media_album2};
3.6.onStart完成数据的初始化
- @Override
- public void onStart(Intent intent) {
- super.onStart(intent);
- super.setUIContent(ResourceTable.Layout_main_ability_slice);
- initComponents();
- initMedia();
- updateUI();
- }
初始化界面组件,实现对应按钮的监听事件
播放或暂停、上一首、下一首、迁移以及进度条的进度变化事件的监听
- /**
- * 初始化界面组件,实现对应按钮的监听事件
- * 播放或暂停、上一首、下一首、迁移以及进度条的进度变化事件的监听
- */
- private void initComponents() {
- LogUtil.debug(TAG, "initComponents");
- musicNameText = (Text) findComponentById(ResourceTable.Id_music_name);
- currentTimeText = (Text) findComponentById(ResourceTable.Id_play_progress_time);
- totalTimeText = (Text) findComponentById(ResourceTable.Id_play_total_time);
- musicPosters = (Image) findComponentById(ResourceTable.Id_music_posters);
- musicPlayButton = (Image) findComponentById(ResourceTable.Id_music_play_btn);
- findComponentById(ResourceTable.Id_remote_play).setClickedListener(this::continueAbility);
- findComponentById(ResourceTable.Id_music_play_prev_btn).setClickedListener(this::prevMusic);
- findComponentById(ResourceTable.Id_music_play_next_btn).setClickedListener(this::nextMusic);
- musicPlayButton.setClickedListener(this::playOrPauseMusic);
- //
- slider = (Slider) findComponentById(ResourceTable.Id_play_progress_bar);
- slider.setValueChangedListener(new ValueChangedListenerImpl());
- }
- private void continueAbility(Component component) {
- try {
- continueAbility();
- } catch (IllegalStateException e) {
- LogUtil.info(TAG, e.getMessage());
- }
- }
- /**
- * 上一首
- * @param component
- */
- private void prevMusic(Component component) {
- currentPos = currentPos == 0 ? 1 : 0;
- currentUri = musics[currentPos];
- //
- playerManager.switchMusic(currentUri);
- //总时长
- totalTime=playerManager.getTotalTime();
- }
- /**
- * 下一首
- * @param component
- */
- private void nextMusic(Component component) {
- currentPos = currentPos == 0 ? 1 : 0;
- currentUri = musics[currentPos];
- //切换音乐
- playerManager.switchMusic(currentUri);
- //总时长
- totalTime=playerManager.getTotalTime();
- }
- /**
- * 播放或暂停音乐
- * @param component
- */
- private void playOrPauseMusic(Component component) {
- //
- playOrPause();
- }
- /**
- * 播放或暂停
- */
- private void playOrPause() {
- LogUtil.debug(TAG, "playOrPause,playerManager:"+playerManager);
- try {
- //
- if (playerManager.isPlaying()) {
- LogUtil.debug(TAG, "playOrPause pause");
- playerManager.pause();
- }else{
- //设置资源
- playerManager.setResource(currentUri);
- //设置进度
- playerManager.rewindTo(currentTime);
- playerManager.play();
- LogUtil.debug(TAG, "playOrPause play");
- }
- } catch (Exception e) {
- LogUtil.error(TAG, "playOrPause");
- e.printStackTrace();
- }
- }
3.7.初始化媒体对象
当前播放歌曲资源,播放器管理者
- /**
- * 初始化媒体对象
- * 当前播放歌曲资源
- * 播放器管理者
- */
- private void initMedia() {
- LogUtil.debug(TAG, "initMedia");
- //当前媒体URI
- currentUri = musics[currentPos];
- LogUtil.debug(TAG, "initMedia,currentUri:"+currentUri);
- //初始化playerManager
- playerManager = new PlayerManager(getApplicationContext(), currentUri);
- //弱引用对象,不会阻止它们的引用对象被终结、终结和回收。 弱引用最常用于实现规范化映射。
- WeakReference<PlayerStateListener> playerStateListener = new WeakReference<>(this);
- //设置状态监听器
- playerManager.setPlayerStateListener(playerStateListener.get());
- //初始化播放器信息
- playerManager.init();
- LogUtil.debug(TAG, "initMedia FINISH");
- }
3.8.远端迁移后恢复播放界面
恢复播放器的播放进度、播放状态、海报、当前时间和总时长、slider播放进度
- /**
- * 远端迁移后恢复的播放,恢复播放器的播放进度
- * 更新UI界面
- */
- private void updateUI() {
- LogUtil.debug(TAG, "updateUI");
- //海报
- musicPosters.setPixelMap(posters[currentPos]);
- //当前时间和总时长
- currentTimeText.setText(getTime(currentTime));
- totalTimeText.setText(getTime(playerManager.getTotalTime()));
- //播放进度
- slider.setMaxValue(playerManager.getTotalTime());
- slider.setProgressValue(currentTime);
- //总时长
- totalTime=playerManager.getTotalTime();
- //远端迁移恢复
- if (isInteractionPlay) {
- LogUtil.debug(TAG, "remotePlay,rewindTo:"+currentTime);
- playerManager.rewindTo(currentTime);
- if (!isPlaying) {
- return;
- }
- //播放
- playerManager.play();
- }
- }
问题总结
1.onMusicFinished 音乐播放完成时应该被调用,但是多数没被调用,只是偶尔会调用,难道是我电脑性能跟不上了?
2.优化了源码中应用启动后,点击播放无法播放的问题
3.优化了播放器播放完当前歌曲更新播放图标
4.增加了相关的注释说明
附件直接下载DistributedMusicPlayer.zip