HarmonyOS Sample 之 DistributedMusicPlayer分布式音乐播放器 原创 精华
@toc
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.增加了相关的注释说明
完整代码
附件直接下载
看着这详细的注释,膜拜一波。
注释详细,大佬可以出书出视频了。
大佬,这个怎么加个播放列表