HarmonyOS Sample 之 DistributedMusicPlayer分布式音乐播放器 原创 精华

Buty9147
发布于 2021-8-16 18:11
浏览
10收藏

@toc

DistributedMusicPlayer分布式音乐播放器

介绍

本示例主要演示了如何通过迁移数据进行音乐的分布式播放。实现了音乐播放的跨设备迁移,包括:播放哪首歌曲、播放进度、以及播放状态的保持。

效果展示

HarmonyOS Sample 之 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 98.64M 159次下载
已于2021-8-17 09:07:31修改
13
收藏 10
回复
举报
3条回复
按时间正序
/
按时间倒序
mb609898e2cfb86
mb609898e2cfb86

看着这详细的注释,膜拜一波。

1
回复
2021-8-16 18:28:22
甜甜爱开发
甜甜爱开发

注释详细,大佬可以出书出视频了。

回复
2021-11-15 09:51:31
永恒的时间
永恒的时间

大佬,这个怎么加个播放列表

回复
2021-11-19 16:58:47
回复
    相关推荐